Modifier le paramètre de demande avec le filtre de servlet


114

Une application Web existante s'exécute sur Tomcat 4.1. Il y a un problème XSS avec une page, mais je ne peux pas modifier la source. J'ai décidé d'écrire un filtre de servlet pour nettoyer le paramètre avant qu'il ne soit vu par la page.

Je voudrais écrire une classe Filter comme celle-ci:

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

Mais ServletRequest.setParametern'existe pas.

Comment puis-je modifier la valeur du paramètre de demande avant de transmettre la demande dans la chaîne?


HttpServletRequestWrapper a beaucoup d'API définies.J'essaie de comprendre chaque API de manière significative.Javadoc n'aide pas à comprendre les API comme 'userinRole', 'getPrincipal'etx., Où exactement puis-je obtenir de l'aide?
sskumar86

Réponses:


132

Comme vous l'avez noté, il HttpServletRequestn'y a pas de méthode setParameter. Ceci est délibéré, car la classe représente la demande telle qu'elle provient du client et la modification du paramètre ne le représenterait pas.

Une solution consiste à utiliser la HttpServletRequestWrapperclasse, qui vous permet d'encapsuler une requête avec une autre. Vous pouvez sous-classer cela et remplacer la getParameterméthode pour renvoyer votre valeur filtrée. Vous pouvez ensuite transmettre cette demande encapsulée à la chain.doFilterplace de la demande d'origine.

C'est un peu moche, mais c'est ce que l'API de servlet dit que vous devriez faire. Si vous essayez de transmettre autre chose à doFilter, certains conteneurs de servlet se plaindront que vous avez enfreint la spécification et refuseront de le gérer.

Une solution plus élégante demande plus de travail - modifiez le servlet / JSP d'origine qui traite le paramètre, de sorte qu'il attend un attribut de requête au lieu d'un paramètre. Le filtre examine le paramètre, le nettoie et définit l'attribut (en utilisant request.setAttribute) avec la valeur filtrée. Pas de sous-classement, pas d'usurpation d'identité, mais vous oblige à modifier d'autres parties de votre application.


6
HttpServletRequestWrapper est merveilleux; Je n'ai jamais su que cela existait. Merci!
Jeremy Stein

3
Merci pour l'alternative de réglage des attributs! J'ai vu un exemple de code utilisant des wrappers de demande et de réponse dans les servlets et les JSP Head First et je ne pouvais pas croire que la spécification pousse les gens à procéder de cette façon.
Kevin

J'avais contacté mes valeurs dans le contrôleur et j'ai défini le paramètre (email et pass) ... maintenant comment puis-je remplacer les valeurs dans mon servlet <property name="username" value="somemail@gmail.com" /> //Change email on logging in <property name="password" value="*********" />//Change Password on logging in
UmaShankar

73

Pour mémoire, voici le cours que j'ai fini par écrire:

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
Vous devrez peut-être également tenir compte de la méthode getParameterMap. Peut-être lancer une exception non prise en charge juste pour qu'aucun composant n'utilise la méthode et ignore la logique de nettoyage.
Tom

1
Bon point, Tom. Dans ce cas particulier, j'ai vérifié et j'ai trouvé qu'il n'était pas appelé, mais j'aurais dû l'ajouter par souci d'exhaustivité et pour le bien de la personne suivante. Merci!
Jeremy Stein

13
On dirait que je suis la prochaine personne, Jeremy. J'ai trouvé cet article en recherchant des options pour modifier les données transmises d'une application externe à un servlet tiers. Dans mon cas, le servlet n'appelait pas HTTPServletRequest.getParameter (), getParameterMap () ou même getAttribute () pour obtenir les données de la demande, donc, par essais et erreurs, j'ai déterminé que le servlet appelait HTTPServletRequest.getInputStream () et getQueryString (). Mon conseil à tous ceux qui tentent cette tâche pour les servlets fermés est d'envelopper chaque accesseur dans HTTPServletRequest afin de comprendre ce qui se passe réellement
Fred Sobotka

3
Pour SrpingMVC, vous devrez remplacer getParameterValues ​​() pour tromper Spring. RequestParamMethodArgumentResolver.resovleName () utilise cette méthode, vous obtiendrez donc MissingServletRequestParameterException sans surcharger. Testé sur Spring Boot 1.2.6 avec spring-web 4.1.7.
barryku

10

Écrivez une classe simple qui se sous-calcule HttpServletRequestWrapperavec une méthode getParameter () qui renvoie la version filtrée de l'entrée. Passez ensuite directement une instance de votre HttpServletRequestWrapperà Filter.doChain()au lieu de l'objet de requête.


1

J'ai eu le même problème (changer un paramètre de la requête HTTP dans le filtre). J'ai fini par utiliser un fichier ThreadLocal<String>. Dans le Filterj'ai:

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

Dans mon processeur de requêtes ( HttpServlet, contrôleur JSF ou tout autre processeur de requêtes HTTP), je récupère la valeur de thread actuelle:

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

Avantages:

  • plus polyvalent que de passer des paramètres HTTP (vous pouvez passer des objets POJO)
  • légèrement plus rapide (pas besoin d'analyser l'URL pour extraire la valeur de la variable)
  • plus élégant Thant le passe- HttpServletRequestWrapperpartout
  • la portée de la variable est plus large que la simple requête HTTP (la portée que vous avez en faisant request.setAttribute(String,Object), c'est-à-dire que vous pouvez accéder à la variable dans d'autres filtreurs.

Désavantages:

  • Vous ne pouvez utiliser cette méthode que lorsque le thread qui traite le filtre est le même que celui qui traite la requête HTTP (c'est le cas de tous les serveurs Java que je connais). Par conséquent, cela ne fonctionnera pas lorsque
    • faire une redirection HTTP (car le navigateur fait une nouvelle requête HTTP et il n'y a aucun moyen de garantir qu'elle sera traitée par le même thread)
    • le traitement des données dans des threads séparés , par exemple lors de l' utilisation java.util.stream.Stream.parallel, java.util.concurrent.Future, java.lang.Thread.
  • Vous devez être en mesure de modifier le processeur de demande / l'application

Quelques notes secondaires:

  • Le serveur dispose d'un pool de threads pour traiter les requêtes HTTP. Puisqu'il s'agit d'une piscine:

    1. un thread de ce pool de threads traitera de nombreuses requêtes HTTP, mais une seule à la fois (vous devez donc soit nettoyer votre variable après utilisation, soit la définir pour chaque requête HTTP = faites attention au code tel que if (value!=null) { THREAD_VARIABLE.set(value);} parce que vous allez réutiliser la valeur de la requête HTTP précédente quand valueest nul: les effets secondaires sont garantis).
    2. Il n'y a aucune garantie que deux requêtes seront traitées par le même thread (cela peut être le cas mais vous n'avez aucune garantie). Si vous avez besoin de conserver les données utilisateur d'une requête à une autre, il serait préférable d'utiliserHttpSession.setAttribute()
  • Le JEE @RequestScopedutilise en interne a ThreadLocal, mais son utilisation ThreadLocalest plus polyvalente: vous pouvez l'utiliser dans des conteneurs non JEE / CDI (par exemple dans des applications JRE multithread)

Est-ce vraiment une bonne idée de définir un paramètre dans la portée du thread? Est-ce que plusieurs demandes verront jamais le même fil de discussion? (Je suppose que non)
Zachary Craig

Est-ce une bonne idée = oui (mais vous devez savoir ce que vous faites, de toute façon le JEE @RequestScopedfait la même chose en interne). Les requêtes multiples verront-elles le même fil = non (ou du moins vous n'avez aucune garantie). J'ai édité la réponse pour préciser ces points.
Julien Kronegg

1

C'est ce que j'ai fini par faire

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

Sur la base de toutes vos remarques, voici ma proposition qui a fonctionné pour moi:

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

    }

note: queryString () nécessite de traiter TOUTES les valeurs de chaque KEY et n'oubliez pas d'encodeUrl () lors de l'ajout de vos propres valeurs de param, si nécessaire

En guise de limitation, si vous appelez request.getParameterMap () ou toute autre méthode qui appellerait request.getReader () et commencer à lire, vous éviterez tout autre appel à request.setCharacterEncoding (...)


0

Vous pouvez utiliser l' expression régulière pour la désinfection. À l'intérieur du filtre avant d'appeler la méthode chain.doFilter (demande, réponse) , appelez ce code. Voici un exemple de code:

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
Vous ne modifiez pas les paramètres de la requête d'origine de cette manière, mais sur une copie.
Mehdi

-1

Essayez request.setAttribute("param",value);. Cela a bien fonctionné pour moi.

Veuillez trouver cet exemple de code:

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
        }
    }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.