Modificar parâmetro de solicitação com filtro de servlet

114

Um aplicativo da web existente está sendo executado no Tomcat 4.1. Há um problema de XSS com uma página, mas não consigo modificar a fonte. Decidi escrever um filtro de servlet para limpar o parâmetro antes de ser visto pela página.

Eu gostaria de escrever uma classe Filter como esta:

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

Mas ServletRequest.setParameternão existe.

Como posso alterar o valor do parâmetro do pedido antes de passar o pedido pela cadeia?

Jeremy Stein
fonte
HttpServletRequestWrapper tem muitas APIs definidas. Estou tentando entender cada API de forma significativa. O Javadoc não está ajudando a entender APIs como 'userinRole', 'getPrincipal'etx., Onde exatamente posso obter ajuda?
sskumar86 de

Respostas:

132

Como você observou HttpServletRequest, não tem um método setParameter. Isso é proposital, já que a classe representa a solicitação como ela veio do cliente, e modificar o parâmetro não representaria isso.

Uma solução é usar a HttpServletRequestWrapperclasse, que permite agrupar uma solicitação com outra. Você pode getParametercriar uma subclasse disso e substituir o método para retornar seu valor higienizado. Você pode então passar essa solicitação encapsulada para, em chain.doFiltervez da solicitação original.

É um pouco feio, mas é o que a API do servlet diz que você deve fazer. Se você tentar passar qualquer outra coisa para doFilter, alguns contêineres de servlet reclamarão que você violou a especificação e se recusarão a lidar com isso.

Uma solução mais elegante é mais trabalhosa - modifique o servlet / JSP original que processa o parâmetro, de forma que ele espere um atributo de solicitação em vez de um parâmetro. O filtro examina o parâmetro, limpa-o e define o atributo (usando request.setAttribute) com o valor limpo. Sem subclasses, sem spoofing, mas exige que você modifique outras partes de seu aplicativo.

skaffman
fonte
6
HttpServletRequestWrapper é maravilhoso; Nunca soube que existia. Obrigado!
Jeremy Stein,
3
Obrigado pela alternativa de configuração de atributos! Viu o código de amostra usando wrappers de solicitação e resposta em Servlets e JSPs do Head First e não pude acreditar que a especificação leva as pessoas a fazer as coisas dessa maneira.
Kevin
Eu havia <property name="username" value="[email protected]" /> //Change email on logging in <property name="password" value="*********" />//Change Password on logging in
entrado em contato
73

Para registro, aqui está a aula que acabei escrevendo:

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) {
    }
}
Jeremy Stein
fonte
5
Você também pode precisar considerar o método getParameterMap. Talvez lance uma exceção sem suporte para que nenhum componente use o método e ignore a lógica de limpeza.
Tom,
1
Bem pensado, Tom. Neste caso particular, verifiquei e descobri que não estava sendo chamado, mas deveria ter adicionado isso para completar e para o bem da próxima pessoa. Obrigado!
Jeremy Stein,
13
Parece que sou a próxima pessoa, Jeremy. Eu encontrei esta postagem ao procurar opções para modificar os dados sendo passados ​​de um aplicativo externo para um servlet de terceiros. No meu caso, o servlet não estava chamando HTTPServletRequest.getParameter (), getParameterMap () ou mesmo getAttribute () para obter os dados da solicitação, então, por tentativa e erro, determinei que o servlet estava chamando HTTPServletRequest.getInputStream () e getQueryString (). Meu conselho para qualquer um que tente esta tarefa para servlets fechados é envolver cada acessador em HTTPServletRequest para entender o que realmente está acontecendo
Fred Sobotka
3
Para SrpingMVC, você precisará substituir getParameterValues ​​() para enganar o Spring. RequestParamMethodArgumentResolver.resovleName () usa esse método, então você obterá MissingServletRequestParameterException sem substituir. Testado no Spring Boot 1.2.6 com spring-web 4.1.7.
barryku
10

Escreva uma classe simples HttpServletRequestWrappercom subcálculo com um método getParameter () que retorna a versão sanitizada da entrada. Em seguida, passe uma instância de seu HttpServletRequestWrapperpara em Filter.doChain()vez do objeto de solicitação diretamente.

Asaph
fonte
1

Tive o mesmo problema (alterando um parâmetro da solicitação HTTP no Filtro). Acabei usando um ThreadLocal<String>. No Filtertenho:

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

No meu processador de solicitação ( HttpServletcontrolador JSF ou qualquer outro processador de solicitação HTTP), recebo o valor do thread atual de volta:

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

Vantagens:

  • mais versátil do que passar parâmetros HTTP (você pode passar objetos POJO)
  • ligeiramente mais rápido (não há necessidade de analisar o URL para extrair o valor da variável)
  • mais elegante do que o HttpServletRequestWrapperclichê
  • o escopo da variável é mais amplo do que apenas a solicitação HTTP (o escopo que você tem ao fazer request.setAttribute(String,Object), ou seja, você pode acessar a variável em outros filtros.

Desvantagens:

  • Você pode usar este método apenas quando o thread que processa o filtro é o mesmo que processa a solicitação HTTP (este é o caso em todos os servidores baseados em Java que eu conheço). Consequentemente, isso não funcionará quando
    • fazer um redirecionamento HTTP (porque o navegador faz uma nova solicitação HTTP e não há como garantir que será processado pelo mesmo thread)
    • processamento de dados em segmentos separados , por exemplo, quando se utiliza java.util.stream.Stream.parallel, java.util.concurrent.Future, java.lang.Thread.
  • Você deve ser capaz de modificar o processador / aplicativo da solicitação

Algumas notas laterais:

  • O servidor possui um pool de threads para processar as solicitações HTTP. Uma vez que esta é uma piscina:

    1. um Thread deste pool de threads processará muitas solicitações HTTP, mas apenas uma de cada vez (então você precisa limpar sua variável após o uso ou defini-la para cada solicitação HTTP = preste atenção ao código if (value!=null) { THREAD_VARIABLE.set(value);}porque você reutilizará o valor da solicitação HTTP anterior quando valueé nulo: os efeitos colaterais são garantidos).
    2. Não há garantia de que duas solicitações serão processadas pelo mesmo thread (pode ser o caso, mas você não tem garantia). Se você precisar manter os dados do usuário de uma solicitação para outra, seria melhor usarHttpSession.setAttribute()
  • O JEE @RequestScopedusa internamente um ThreadLocal, mas usar o ThreadLocalé mais versátil: você pode usá-lo em contêineres não JEE / CDI (por exemplo, em aplicativos JRE multithread)
Julien Kronegg
fonte
É realmente uma boa ideia definir um parâmetro no escopo do thread? As solicitações múltiplas verão o mesmo segmento? (Suponho que não)
Zachary Craig
É uma boa ideia = sim (mas você precisa saber o que está fazendo, de qualquer maneira o JEE @RequestScopedfaz o mesmo internamente). A solicitação múltipla verá o mesmo tópico = não (ou pelo menos você não tem garantia). Eu editei a resposta para precisar esses pontos.
Julien Kronegg
1

Isso é o que eu acabei fazendo

//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();
        }
    }
}
agente 47
fonte
1

Com base em todos os seus comentários, aqui está minha proposta que funcionou para mim:

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

    }

observação: queryString () requer o processamento de TODOS os valores para cada KEY e não se esqueça de encodeUrl () ao adicionar seus próprios valores de parâmetros, se necessário

Como limitação, se você chamar request.getParameterMap () ou qualquer método que chame request.getReader () e começar a ler, você evitará outras chamadas para request.setCharacterEncoding (...)

benGiacomo
fonte
0

Você pode usar Regular Expression for Sanitization. Filtro interno antes de chamar o método chain.doFilter (solicitação, resposta) , chame este código. Aqui está o código de amostra:

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();   
    }
}
Ajinkya Peshave
fonte
1
Você não modifica os parâmetros da solicitação original dessa maneira, mas em uma cópia.
Mehdi
-1

Experimente request.setAttribute("param",value);. Funcionou bem para mim.

Encontre este exemplo de código:

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
        }
    }
Bolsa Ankur
fonte