Modifica il parametro di richiesta con il filtro servlet


114

Un'applicazione Web esistente è in esecuzione su Tomcat 4.1. C'è un problema XSS con una pagina, ma non riesco a modificare l'origine. Ho deciso di scrivere un filtro servlet per disinfettare il parametro prima che venga visto dalla pagina.

Vorrei scrivere una classe Filter come questa:

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) {
  }
}

Ma ServletRequest.setParameternon esiste.

Come posso modificare il valore del parametro di richiesta prima di passare la richiesta alla catena?


HttpServletRequestWrapper ha molte API definite.Sto cercando di capire ogni singola API in modo significativo.Javadoc non sta aiutando a capire API come 'userinRole', 'getPrincipal'etx., Dove posso ottenere aiuto esattamente?
sskumar86

Risposte:


132

Come hai notato HttpServletRequestnon ha un metodo setParameter. Ciò è intenzionale, poiché la classe rappresenta la richiesta così come proviene dal client e la modifica del parametro non lo rappresenterebbe.

Una soluzione è utilizzare la HttpServletRequestWrapperclasse, che consente di racchiudere una richiesta con un'altra. Puoi sottoclassarlo e sovrascrivere il getParametermetodo per restituire il tuo valore disinfettato. È quindi possibile passare quella richiesta di wrapping a chain.doFilterinvece della richiesta originale.

È un po 'brutto, ma è quello che l'API servlet dice che dovresti fare. Se provi a passare qualcos'altro a doFilter, alcuni contenitori servlet si lamenteranno che hai violato le specifiche e si rifiuteranno di gestirlo.

Una soluzione più elegante è più lavoro: modificare il servlet / JSP originale che elabora il parametro, in modo che si aspetti un attributo di richiesta invece di un parametro. Il filtro esamina il parametro, lo disinfetta e imposta l'attributo (utilizzando request.setAttribute) con il valore disinfettato. Nessuna sottoclasse, nessuno spoofing, ma richiede la modifica di altre parti dell'applicazione.


6
HttpServletRequestWrapper è meraviglioso; Non ho mai saputo che esistesse. Grazie!
Jeremy Stein,

3
Grazie per l'alternativa di impostazione degli attributi! Ho visto codice di esempio utilizzando wrapper di richieste e risposte in Head First Servlet e JSP e non potevo credere che le specifiche spingessero le persone a fare le cose in quel modo.
Kevin

Avevo contattato i miei valori nel controller e ho impostato il parametro (email e pass) ... ora come posso sostituire i valori nel mio servlet <property name="username" value="somemail@gmail.com" /> //Change email on logging in <property name="password" value="*********" />//Change Password on logging in
UmaShankar

73

Per la cronaca, ecco la classe che ho finito per scrivere:

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
Potrebbe anche essere necessario tenere conto del metodo getParameterMap. Forse lancia un'eccezione non supportata solo in modo che nessun componente utilizzi il metodo e salti la logica di disinfezione.
Tom

1
Buon punto, Tom. In questo caso particolare, ho controllato e ho scoperto che non veniva chiamato, ma avrei dovuto aggiungerlo per completezza e per il bene della persona successiva. Grazie!
Jeremy Stein

13
Sembra che io sia la prossima persona, Jeremy. Ho trovato questo post mentre cercavo opzioni per modificare i dati passati da un'applicazione esterna a un servlet di terze parti. Nel mio caso, il servlet non stava chiamando HTTPServletRequest.getParameter (), getParameterMap () o anche getAttribute () per ottenere i dati della richiesta, quindi, attraverso tentativi ed errori, ho determinato che il servlet stava chiamando HTTPServletRequest.getInputStream () e getQueryString (). Il mio consiglio a chiunque tenti di eseguire questa attività per servlet chiusi è di avvolgere ogni singola funzione di accesso in HTTPServletRequest per capire cosa sta realmente accadendo
Fred Sobotka

3
Per SrpingMVC, dovrai sovrascrivere getParameterValues ​​() per ingannare Spring. RequestParamMethodArgumentResolver.resovleName () utilizza quel metodo, quindi otterrai MissingServletRequestParameterException senza sovrascrivere. Testato su Spring Boot 1.2.6 con spring-web 4.1.7.
barryku

10

Scrivete una classe semplice che esegua HttpServletRequestWrapperuna sottocale con un metodo getParameter () che restituisca la versione disinfettata dell'input. Quindi passa direttamente una tua istanza HttpServletRequestWrappera Filter.doChain()invece dell'oggetto richiesta.


1

Ho avuto lo stesso problema (cambiando un parametro dalla richiesta HTTP nel filtro). Ho finito per usare un file ThreadLocal<String>. In Filterho:

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);
    }
}

Nel mio processore di richiesta ( HttpServlet, controller JSF o qualsiasi altro processore di richiesta HTTP), ottengo indietro il valore del thread corrente:

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

vantaggi:

  • più versatile del passaggio di parametri HTTP (puoi passare oggetti POJO)
  • leggermente più veloce (non è necessario analizzare l'URL per estrarre il valore della variabile)
  • più elegante del HttpServletRequestWrapperboilerplate
  • l'ambito della variabile è più ampio della semplice richiesta HTTP (l'ambito che hai quando lo fai request.setAttribute(String,Object), cioè puoi accedere alla variabile in altri filtri.

svantaggi:

  • È possibile utilizzare questo metodo solo quando il thread che elabora il filtro è lo stesso di quello che elabora la richiesta HTTP (questo è il caso di tutti i server basati su Java che conosco). Di conseguenza, questo non funzionerà quando
    • fare un reindirizzamento HTTP (perché il browser esegue una nuova richiesta HTTP e non c'è modo di garantire che verrà elaborata dallo stesso thread)
    • l'elaborazione dei dati in thread separati , ad esempio quando si usa java.util.stream.Stream.parallel, java.util.concurrent.Future, java.lang.Thread.
  • Devi essere in grado di modificare il processore / applicazione della richiesta

Alcune note a margine:

  • Il server dispone di un pool di thread per elaborare le richieste HTTP. Poiché questo è pool:

    1. un thread da questo pool di thread elaborerà molte richieste HTTP, ma solo una alla volta (quindi è necessario pulire la variabile dopo l'uso o definirla per ogni richiesta HTTP = prestare attenzione al codice come if (value!=null) { THREAD_VARIABLE.set(value);}perché si riutilizzerà il valore dalla precedente richiesta HTTP quando valueè nullo: gli effetti collaterali sono garantiti).
    2. Non vi è alcuna garanzia che due richieste verranno elaborate dallo stesso thread (potrebbe essere il caso ma non si ha alcuna garanzia). Se è necessario mantenere i dati dell'utente da una richiesta a un'altra richiesta, sarebbe meglio usareHttpSession.setAttribute()
  • Il JEE @RequestScopedutilizza internamente un ThreadLocal, ma l'uso di ThreadLocalè più versatile: puoi usarlo in contenitori non JEE / CDI (ad esempio in applicazioni JRE multithread)

È davvero una buona idea impostare un parametro nell'ambito del thread? Più richieste vedranno mai lo stesso thread? (Presumo di no)
Zachary Craig

È una buona idea = sì (ma devi sapere cosa stai facendo, comunque il JEE @RequestScopedfa internamente lo stesso). La richiesta multipla vedrà lo stesso thread = no (o almeno non hai alcuna garanzia). Ho modificato la risposta per precisare questi punti.
Julien Kronegg

1

Questo è quello che ho finito per fare

//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

Sulla base di tutte le tue osservazioni, ecco la mia proposta che ha funzionato per me:

 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");
        }

    }

nota: queryString () richiede di elaborare TUTTI i valori per ogni CHIAVE e non dimenticare di encodeUrl () quando si aggiungono i propri valori di parametro, se necessario

Come limitazione, se chiami request.getParameterMap () o qualsiasi metodo che chiamerebbe request.getReader () e inizi a leggere, impedirai ulteriori chiamate a request.setCharacterEncoding (...)


0

È possibile utilizzare Regular Expression per Sanitization. All'interno del filtro prima di chiamare il metodo chain.doFilter (request, response) , chiama questo codice. Ecco il codice di esempio:

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
Non si modificano i parametri della richiesta originale in questo modo, ma su una copia.
Mehdi

-1

Prova request.setAttribute("param",value);. Ha funzionato bene per me.

Trova questo esempio di codice:

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
        }
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.