Zmodyfikuj parametr żądania za pomocą filtru serwletu


114

Istniejąca aplikacja internetowa działa na Tomcat 4.1. Wystąpił problem z XSS na stronie, ale nie mogę zmodyfikować źródła. Zdecydowałem się napisać filtr serwletów, aby oczyścić parametr, zanim zostanie on zauważony przez stronę.

Chciałbym napisać taką klasę filtru:

import java.io.*;
import javax.servlet.*;

public final class XssFilter implements Filter {

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException
  {
    String badValue = request.getParameter("dangerousParamName");
    String goodValue = sanitize(badValue);
    request.setParameter("dangerousParamName", goodValue);
    chain.doFilter(request, response);
  }

  public void destroy() {
  }

  public void init(FilterConfig filterConfig) {
  }
}

Ale ServletRequest.setParameternie istnieje.

Jak mogę zmienić wartość parametru żądania przed przekazaniem żądania w dół łańcucha?


HttpServletRequestWrapper ma zdefiniowane wiele interfejsów API. Próbuję zrozumieć każde API w znaczący sposób. Javadoc nie pomaga w zrozumieniu interfejsów API, takich jak 'userinRole', 'getPrincipal'etx., Gdzie dokładnie mogę uzyskać pomoc?
sskumar86

Odpowiedzi:


132

Jak zauważyłeś HttpServletRequest, nie ma metody setParameter. Jest to celowe, ponieważ klasa reprezentuje żądanie w takiej postaci, w jakiej pochodziło od klienta, a modyfikacja parametru tego nie reprezentuje.

Jednym z rozwiązań jest użycie HttpServletRequestWrapperklasy, która pozwala zawijać jedno żądanie drugim. Możesz to podklasować i zastąpić getParametermetodę, aby zwrócić oczyszczoną wartość. Następnie możesz przekazać to opakowane żądanie chain.doFilterzamiast pierwotnego żądania.

To trochę brzydkie, ale to właśnie mówi API serwletów, co powinieneś zrobić. Jeśli spróbujesz przekazać cokolwiek innego doFilter, niektóre kontenery serwletów będą narzekać, że naruszyłeś specyfikację, i odmówią obsługi tego.

Bardziej eleganckie rozwiązanie to więcej pracy - zmodyfikuj oryginalny serwlet / JSP, który przetwarza parametr, tak aby oczekiwał atrybutu żądania zamiast parametru. Filtr sprawdza parametr, oczyszcza go i ustawia atrybut (używając request.setAttribute) na wartość oczyszczoną. Bez podklas, bez fałszowania, ale wymaga modyfikacji innych części aplikacji.


6
HttpServletRequestWrapper jest wspaniały; Nigdy nie wiedziałem, że istnieje. Dzięki!
Jeremy Stein

3
Dzięki za alternatywę ustawiania atrybutów! Widziałem przykładowy kod używający opakowań żądań i odpowiedzi w serwletach Head First i JSP i nie mogłem uwierzyć, że specyfikacja popycha ludzi do tego.
Kevin

Dotarłem do moich wartości w kontrolerze i ustawiłem parametr (e-mail i pass) ... teraz jak mogę zastąpić wartości w moim serwlecie <property name="username" value="somemail@gmail.com" /> //Change email on logging in <property name="password" value="*********" />//Change Password on logging in
UmaShankar

73

Dla przypomnienia, oto klasa, którą napisałem:

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public final class XssFilter implements Filter {

    static class FilteredRequest extends HttpServletRequestWrapper {

        /* These are the characters allowed by the Javascript validation */
        static String allowedChars = "+-0123456789#*";

        public FilteredRequest(ServletRequest request) {
            super((HttpServletRequest)request);
        }

        public String sanitize(String input) {
            String result = "";
            for (int i = 0; i < input.length(); i++) {
                if (allowedChars.indexOf(input.charAt(i)) >= 0) {
                    result += input.charAt(i);
                }
            }
            return result;
        }

        public String getParameter(String paramName) {
            String value = super.getParameter(paramName);
            if ("dangerousParamName".equals(paramName)) {
                value = sanitize(value);
            }
            return value;
        }

        public String[] getParameterValues(String paramName) {
            String values[] = super.getParameterValues(paramName);
            if ("dangerousParamName".equals(paramName)) {
                for (int index = 0; index < values.length; index++) {
                    values[index] = sanitize(values[index]);
                }
            }
            return values;
        }
    }

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new FilteredRequest(request), response);
    }

    public void destroy() {
    }

    public void init(FilterConfig filterConfig) {
    }
}

5
Konieczne może być również uwzględnienie metody getParameterMap. Może zgłosić i nieobsługiwany wyjątek, aby żadne komponenty nie używały tej metody i pominęły logikę sanitize.
Tom

1
Słuszna uwaga, Tom. W tym konkretnym przypadku sprawdziłem i stwierdziłem, że nie jest wywoływany, ale powinienem dodać to dla kompletności i ze względu na następną osobę. Dzięki!
Jeremy Stein

13
Wygląda na to, że jestem następną osobą, Jeremy. Znalazłem ten post, szukając opcji modyfikacji danych przekazywanych z zewnętrznej aplikacji do serwletu innej firmy. W moim przypadku serwlet nie wywoływał HTTPServletRequest.getParameter (), getParameterMap () ani nawet getAttribute () w celu uzyskania danych żądania, więc metodą prób i błędów ustaliłem, że aplet wywołuje HTTPServletRequest.getInputStream () i getQueryString (). Moja rada dla każdego, kto próbuje wykonać to zadanie dla zamkniętych serwletów, to opakować każdy dostęp w HTTPServletRequest, aby zrozumieć, co się naprawdę dzieje
Fred Sobotka

3
W przypadku SrpingMVC trzeba będzie zastąpić metodę getParameterValues ​​(), aby oszukać Spring. RequestParamMethodArgumentResolver.resovleName () używa tej metody, więc otrzymasz MissingServletRequestParameterException bez nadpisywania. Testowano na Spring Boot 1.2.6 ze sprężynami 4.1.7.
barryku

10

Napisz prostą klasę, która poddaje podskalowanie HttpServletRequestWrapperza pomocą metody getParameter (), która zwraca oczyszczoną wersję danych wejściowych. Następnie przekaż swoje wystąpienie bezpośrednio HttpServletRequestWrapperdo Filter.doChain()zamiast obiektu żądania.


1

Miałem ten sam problem (zmiana parametru z żądania HTTP w filtrze). Skończyło się na użyciu ThreadLocal<String>. W Filtermam:

class MyFilter extends Filter {
    public static final ThreadLocal<String> THREAD_VARIABLE = new ThreadLocal<>();
    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
        THREAD_VARIABLE.set("myVariableValue");
        chain.doFilter(request, response);
    }
}

W moim procesorze żądań ( HttpServletkontrolerze JSF lub jakimkolwiek innym procesorze żądań HTTP) otrzymuję z powrotem aktualną wartość wątku:

...
String myVariable = MyFilter.THREAD_VARIABLE.get();
...

Zalety:

  • bardziej wszechstronny niż przekazywanie parametrów HTTP (można przekazywać obiekty POJO)
  • nieco szybciej (nie trzeba analizować adresu URL w celu wyodrębnienia wartości zmiennej)
  • bardziej elegancki niż HttpServletRequestWrapperszablonowy
  • zakres zmiennej jest szerszy niż tylko żądanie HTTP (zakres, który masz podczas wykonywania request.setAttribute(String,Object), tj. możesz uzyskać dostęp do zmiennej w innych filtrach.

Niedogodności:

  • Możesz użyć tej metody tylko wtedy, gdy wątek przetwarzający filtr jest taki sam, jak ten, który przetwarza żądanie HTTP (dotyczy to wszystkich znanych mi serwerów opartych na Javie). W konsekwencji to nie zadziała, gdy
    • wykonanie przekierowania HTTP (ponieważ przeglądarka wysyła nowe żądanie HTTP i nie ma możliwości zagwarantowania, że ​​będzie ono przetwarzane przez ten sam wątek)
    • Przetwarzanie danych w poszczególnych nitek , na przykład przy użyciu java.util.stream.Stream.parallel, java.util.concurrent.Future, java.lang.Thread.
  • Musisz mieć możliwość modyfikowania procesora / aplikacji żądań

Kilka uwag dodatkowych:

  • Serwer ma pulę wątków do przetwarzania żądań HTTP. Ponieważ to jest pula:

    1. Wątek z tej puli wątków będzie przetwarzał wiele żądań HTTP, ale tylko jedno na raz (więc musisz albo wyczyścić zmienną po użyciu, albo zdefiniować ją dla każdego żądania HTTP = zwróć uwagę na kod, na przykład if (value!=null) { THREAD_VARIABLE.set(value);}ponieważ ponownie użyjesz wartości z poprzedniego żądania HTTP, gdy valuema wartość null: efekty uboczne są gwarantowane).
    2. Nie ma gwarancji, że dwa żądania zostaną przetworzone przez ten sam wątek (może tak być, ale nie masz gwarancji). Jeśli chcesz zachować dane użytkownika z jednego żądania do drugiego, lepiej byłoby użyćHttpSession.setAttribute()
  • JEE @RequestScopedwewnętrznie używa a ThreadLocal, ale użycie ThreadLocaljest bardziej wszechstronne: można go używać w kontenerach innych niż JEE / CDI (np. W wielowątkowych aplikacjach JRE)

Czy to naprawdę dobry pomysł, aby ustawić parametr w zakresie wątku? Czy w przypadku wielu żądań pojawi się ten sam wątek? (Zakładam, że nie)
Zachary Craig

Czy to dobry pomysł = tak (ale musisz wiedzieć, co robisz, w każdym razie JEE @RequestScopedwewnętrznie robi to samo). Czy wiele żądań zobaczy ten sam wątek = nie (lub przynajmniej nie masz żadnej gwarancji). Zredagowałem odpowiedź, aby uściślić te punkty.
Julien Kronegg

1

Oto, co ostatecznie zrobiłem

//import ../../Constants;

public class RequestFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(RequestFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
        try {
            CustomHttpServletRequest customHttpServletRequest = new CustomHttpServletRequest((HttpServletRequest) servletRequest);
            filterChain.doFilter(customHttpServletRequest, servletResponse);
        } finally {
            //do something here
        }
    }



    @Override
    public void destroy() {

    }

     public static Map<String, String[]> ADMIN_QUERY_PARAMS = new HashMap<String, String[]>() {
        {
            put("diagnostics", new String[]{"false"});
            put("skipCache", new String[]{"false"});
        }
    };

    /*
        This is a custom wrapper over the `HttpServletRequestWrapper` which 
        overrides the various header getter methods and query param getter methods.
        Changes to the request pojo are
        => A custom header is added whose value is a unique id
        => Admin query params are set to default values in the url
    */
    private class CustomHttpServletRequest extends HttpServletRequestWrapper {
        public CustomHttpServletRequest(HttpServletRequest request) {
            super(request);
            //create custom id (to be returned) when the value for a
            //particular header is asked for
            internalRequestId = RandomStringUtils.random(10, true, true) + "-local";
        }

        public String getHeader(String name) {
            String value = super.getHeader(name);
            if(Strings.isNullOrEmpty(value) && isRequestIdHeaderName(name)) {
                value = internalRequestId;
            }
            return value;
        }

        private boolean isRequestIdHeaderName(String name) {
            return Constants.RID_HEADER.equalsIgnoreCase(name) || Constants.X_REQUEST_ID_HEADER.equalsIgnoreCase(name);
        }

        public Enumeration<String> getHeaders(String name) {
            List<String> values = Collections.list(super.getHeaders(name));
            if(values.size()==0 && isRequestIdHeaderName(name)) {
                values.add(internalRequestId);
            }
            return Collections.enumeration(values);
        }

        public Enumeration<String> getHeaderNames() {
            List<String> names = Collections.list(super.getHeaderNames());
            names.add(Constants.RID_HEADER);
            names.add(Constants.X_REQUEST_ID_HEADER);
            return Collections.enumeration(names);
        }

        public String getParameter(String name) {
            if (ADMIN_QUERY_PARAMS.get(name) != null) {
                return ADMIN_QUERY_PARAMS.get(name)[0];
            }
            return super.getParameter(name);
        }

        public Map<String, String[]> getParameterMap() {
            Map<String, String[]> paramsMap = new HashMap<>(super.getParameterMap());
            for (String paramName : ADMIN_QUERY_PARAMS.keySet()) {
                if (paramsMap.get(paramName) != null) {
                    paramsMap.put(paramName, ADMIN_QUERY_PARAMS.get(paramName));
                }
            }
            return paramsMap;
        }

        public String[] getParameterValues(String name) {
            if (ADMIN_QUERY_PARAMS.get(name) != null) {
                return ADMIN_QUERY_PARAMS.get(name);
            }
            return super.getParameterValues(name);
        }

        public String getQueryString() {
            Map<String, String[]> map = getParameterMap();
            StringBuilder builder = new StringBuilder();
            for (String param: map.keySet()) {
                for (String value: map.get(param)) {
                    builder.append(param).append("=").append(value).append("&");
                }
            }
            builder.deleteCharAt(builder.length() - 1);
            return builder.toString();
        }
    }
}

1

Na podstawie wszystkich twoich uwag oto moja propozycja, która zadziałała dla mnie:

 private final class CustomHttpServletRequest extends HttpServletRequestWrapper {

    private final Map<String, String[]> queryParameterMap;
    private final Charset requestEncoding;

    public CustomHttpServletRequest(HttpServletRequest request) {
        super(request);
        queryParameterMap = getCommonQueryParamFromLegacy(request.getParameterMap());

        String encoding = request.getCharacterEncoding();
        requestEncoding = (encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8);
    }

    private final Map<String, String[]> getCommonQueryParamFromLegacy(Map<String, String[]> paramMap) {
        Objects.requireNonNull(paramMap);

        Map<String, String[]> commonQueryParamMap = new LinkedHashMap<>(paramMap);

        commonQueryParamMap.put(CommonQueryParams.PATIENT_ID, new String[] { paramMap.get(LEGACY_PARAM_PATIENT_ID)[0] });
        commonQueryParamMap.put(CommonQueryParams.PATIENT_BIRTHDATE, new String[] { paramMap.get(LEGACY_PARAM_PATIENT_BIRTHDATE)[0] });
        commonQueryParamMap.put(CommonQueryParams.KEYWORDS, new String[] { paramMap.get(LEGACY_PARAM_STUDYTYPE)[0] });

        String lowerDateTime = null;
        String upperDateTime = null;

        try {
            String studyDateTime = new SimpleDateFormat("yyyy-MM-dd").format(new SimpleDateFormat("dd-MM-yyyy").parse(paramMap.get(LEGACY_PARAM_STUDY_DATE_TIME)[0]));

            lowerDateTime = studyDateTime + "T23:59:59";
            upperDateTime = studyDateTime + "T00:00:00";

        } catch (ParseException e) {
            LOGGER.error("Can't parse StudyDate from query parameters : {}", e.getLocalizedMessage());
        }

        commonQueryParamMap.put(CommonQueryParams.LOWER_DATETIME, new String[] { lowerDateTime });
        commonQueryParamMap.put(CommonQueryParams.UPPER_DATETIME, new String[] { upperDateTime });

        legacyQueryParams.forEach(commonQueryParamMap::remove);
        return Collections.unmodifiableMap(commonQueryParamMap);

    }

    @Override
    public String getParameter(String name) {
        String[] params = queryParameterMap.get(name);
        return params != null ? params[0] : null;
    }

    @Override
    public String[] getParameterValues(String name) {
        return queryParameterMap.get(name);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
            return queryParameterMap; // unmodifiable to uphold the interface contract.
        }

        @Override
        public Enumeration<String> getParameterNames() {
            return Collections.enumeration(queryParameterMap.keySet());
        }

        @Override
        public String getQueryString() {
            // @see : https://stackoverflow.com/a/35831692/9869013
            // return queryParameterMap.entrySet().stream().flatMap(entry -> Stream.of(entry.getValue()).map(value -> entry.getKey() + "=" + value)).collect(Collectors.joining("&")); // without encoding !!
            return queryParameterMap.entrySet().stream().flatMap(entry -> encodeMultiParameter(entry.getKey(), entry.getValue(), requestEncoding)).collect(Collectors.joining("&"));
        }

        private Stream<String> encodeMultiParameter(String key, String[] values, Charset encoding) {
            return Stream.of(values).map(value -> encodeSingleParameter(key, value, encoding));
        }

        private String encodeSingleParameter(String key, String value, Charset encoding) {
            return urlEncode(key, encoding) + "=" + urlEncode(value, encoding);
        }

        private String urlEncode(String value, Charset encoding) {
            try {
                return URLEncoder.encode(value, encoding.name());
            } catch (UnsupportedEncodingException e) {
                throw new IllegalArgumentException("Cannot url encode " + value, e);
            }
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            throw new UnsupportedOperationException("getInputStream() is not implemented in this " + CustomHttpServletRequest.class.getSimpleName() + " wrapper");
        }

    }

uwaga: queryString () wymaga przetworzenia WSZYSTKICH wartości dla każdego KLUCZA i nie zapomnij o encodeUrl () podczas dodawania własnych wartości parametrów, jeśli jest to wymagane

W ramach ograniczenia, jeśli wywołasz request.getParameterMap () lub jakąkolwiek metodę, która wywoła request.getReader () i zaczniesz czytać, zapobiegniesz dalszym wywołaniom request.setCharacterEncoding (...)


0

Możesz użyć wyrażenia regularnego do odkażania. Wewnątrz filtru przed wywołaniem metody chain.doFilter (żądanie, odpowiedź) wywołaj ten kod. Oto przykładowy kod:

for (Enumeration en = request.getParameterNames(); en.hasMoreElements(); ) {
String name = (String)en.nextElement();
String values[] = request.getParameterValues(name);
int n = values.length;
    for(int i=0; i < n; i++) {
     values[i] = values[i].replaceAll("[^\\dA-Za-z ]","").replaceAll("\\s+","+").trim();   
    }
}

1
Nie modyfikujesz w ten sposób oryginalnych parametrów żądania, ale na kopii.
Mehdi

-1

Spróbuj request.setAttribute("param",value);. U mnie to działało dobrze.

Znajdź ten przykładowy kod:

private void sanitizePrice(ServletRequest request){
        if(request.getParameterValues ("price") !=  null){
            String price[] = request.getParameterValues ("price");

            for(int i=0;i<price.length;i++){
                price[i] = price[i].replaceAll("[^\\dA-Za-z0-9- ]", "").trim();
                System.out.println(price[i]);
            }
            request.setAttribute("price", price);
            //request.getParameter("numOfBooks").re
        }
    }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.