Modificar el parámetro de solicitud con filtro de servlet


114

Una aplicación web existente se está ejecutando en Tomcat 4.1. Hay un problema de XSS con una página, pero no puedo modificar la fuente. Decidí escribir un filtro de servlet para desinfectar el parámetro antes de que lo vea la página.

Me gustaría escribir una clase de filtro 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) {
  }
}

Pero ServletRequest.setParameterno existe.

¿Cómo puedo cambiar el valor del parámetro de solicitud antes de pasar la solicitud por la cadena?


HttpServletRequestWrapper tiene muchas API definidas. Estoy tratando de comprender todas y cada una de las API de manera significativa. Javadoc no está ayudando a comprender las API como 'userinRole', 'getPrincipal'etx., ¿Dónde puedo obtener ayuda exactamente?
sskumar86

Respuestas:


132

Como ha notado HttpServletRequest, no tiene un método setParameter. Esto es deliberado, ya que la clase representa la solicitud tal como proviene del cliente, y modificar el parámetro no lo representaría.

Una solución es usar la HttpServletRequestWrapperclase, que le permite envolver una solicitud con otra. Puede subclasificar eso y anular el getParametermétodo para devolver su valor desinfectado. Luego, puede pasar esa solicitud envuelta en chain.doFilterlugar de la solicitud original.

Es un poco feo, pero eso es lo que la API del servlet dice que debes hacer. Si intenta pasar algo más doFilter, algunos contenedores de servlets se quejarán de que ha violado la especificación y se negarán a manipularla.

Una solución más elegante requiere más trabajo: modifique el servlet / JSP original que procesa el parámetro, para que espere un atributo de solicitud lugar de un parámetro. El filtro examina el parámetro, lo desinfecta y establece el atributo (uso request.setAttribute) con el valor desinfectado. Sin subclases, sin suplantación de identidad, pero requiere que modifique otras partes de su aplicación.


6
HttpServletRequestWrapper es maravilloso; Nunca supe que existía. ¡Gracias!
Jeremy Stein

3
¡Gracias por la alternativa de configuración de atributos! Vi código de muestra usando envoltorios de solicitud y respuesta en Head First Servlets y JSP y no podía creer que la especificación empujara a las personas a hacer las cosas de esa manera.
Kevin

Me comuniqué con mis valores en el controlador y configuré el parámetro (correo electrónico y pase) ... ahora, ¿cómo puedo reemplazar los valores en mi servlet? <property name="username" value="somemail@gmail.com" /> //Change email on logging in <property name="password" value="*********" />//Change Password on logging in
UmaShankar

73

Para que conste, aquí está la clase que terminé escribiendo:

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
Es posible que también deba tener en cuenta el método getParameterMap. Tal vez lanzar una excepción no admitida para que ningún componente use el método y omita la lógica de desinfección.
Tom

1
Buen punto, Tom. En este caso particular, verifiqué y encontré que no estaba siendo llamado, pero debería haber agregado eso para completar y por el bien de la siguiente persona. ¡Gracias!
Jeremy Stein

13
Parece que soy la siguiente persona, Jeremy. Encontré esta publicación cuando buscaba opciones para modificar los datos que se pasan de una aplicación externa a un servlet de terceros. En mi caso, el servlet no estaba llamando a HTTPServletRequest.getParameter (), getParameterMap () o incluso a getAttribute () para obtener los datos de la solicitud, así que, a través de prueba y error, determiné que el servlet estaba llamando a HTTPServletRequest.getInputStream () y getQueryString (). Mi consejo para cualquiera que intente esta tarea para servlets cerrados es encapsular cada accesor en HTTPServletRequest para entender lo que realmente está sucediendo
Fred Sobotka

3
Para SrpingMVC, deberá anular getParameterValues ​​() para engañar a Spring. RequestParamMethodArgumentResolver.resovleName () usa ese método, por lo que obtendrá MissingServletRequestParameterException sin anular. Probado en Spring Boot 1.2.6 con spring-web 4.1.7.
Barryku

10

Escriba una clase simple que subcalifique HttpServletRequestWrappercon un método getParameter () que devuelva la versión desinfectada de la entrada. Luego, pase una instancia de su HttpServletRequestWrapperto en Filter.doChain()lugar del objeto de solicitud directamente.


1

Tuve el mismo problema (cambiar un parámetro de la solicitud HTTP en el Filtro). Terminé usando un ThreadLocal<String>. En el Filtertengo:

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

En mi procesador de solicitudes ( HttpServlet, controlador JSF o cualquier otro procesador de solicitudes HTTP), recupero el valor del hilo actual:

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

Ventajas:

  • más versátil que pasar parámetros HTTP (puede pasar objetos POJO)
  • un poco más rápido (no es necesario analizar la URL para extraer el valor de la variable)
  • más elegante que la HttpServletRequestWrappercaldera
  • el alcance de la variable es más amplio que solo la solicitud HTTP (el alcance que tiene al hacerlo request.setAttribute(String,Object), es decir, puede acceder a la variable en otros filtros.

Desventajas:

  • Puede usar este método solo cuando el hilo que procesa el filtro es el mismo que el que procesa la solicitud HTTP (este es el caso en todos los servidores basados ​​en Java que conozco). En consecuencia, esto no funcionará cuando
    • hacer una redirección HTTP (porque el navegador realiza una nueva solicitud HTTP y no hay forma de garantizar que será procesada por el mismo hilo)
    • el procesamiento de datos en hilos separados , por ejemplo, cuando se utiliza java.util.stream.Stream.parallel, java.util.concurrent.Future, java.lang.Thread.
  • Debe poder modificar el procesador de solicitudes / aplicación

Algunas notas al margen:

  • El servidor tiene un grupo de subprocesos para procesar las solicitudes HTTP. Dado que esto es piscina:

    1. un subproceso de este grupo de subprocesos procesará muchas solicitudes HTTP, pero solo una a la vez (por lo que debe limpiar su variable después del uso o definirla para cada solicitud HTTP = preste atención al código, por ejemplo, if (value!=null) { THREAD_VARIABLE.set(value);}porque reutilizará el valor de la solicitud HTTP anterior cuando valuees nulo: los efectos secundarios están garantizados).
    2. No hay garantía de que dos solicitudes sean procesadas por el mismo hilo (puede ser el caso, pero no tiene garantía). Si necesita mantener los datos del usuario de una solicitud a otra, sería mejor usarHttpSession.setAttribute()
  • El JEE @RequestScopedusa internamente a ThreadLocal, pero usar el ThreadLocales más versátil: puede usarlo en contenedores que no sean JEE / CDI (por ejemplo, en aplicaciones JRE multiproceso)

¿Es realmente una buena idea establecer un parámetro en el alcance del hilo? ¿Verán varias solicitudes el mismo hilo? (Supongo que no)
Zachary Craig

¿Es una buena idea = sí (pero necesita saber lo que está haciendo, de todos modos el JEE @RequestScopedhace lo mismo internamente)? ¿Las solicitudes múltiples verán el mismo hilo = no (o al menos no tiene garantía)? He editado la respuesta para precisar estos puntos.
Julien Kronegg

1

Esto es lo que terminé haciendo

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

Basado en todos sus comentarios, aquí está mi propuesta que funcionó para mí:

 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 () requiere procesar TODOS los valores para cada CLAVE y no olvide encodeUrl () cuando agregue sus propios valores de parámetro, si es necesario

Como limitación, si llama a request.getParameterMap () o cualquier método que llame a request.getReader () y comience a leer, evitará más llamadas a request.setCharacterEncoding (...)


0

Puede utilizar la expresión regular para la desinfección. Dentro del filtro antes de llamar al método chain.doFilter (solicitud, respuesta) , llame a este código. Aquí está el código de muestra:

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
No modifica los parámetros de la solicitud original de esta manera, sino en una copia.
Mehdi

-1

Prueba request.setAttribute("param",value);. Funcionó bien para mí.

Encuentre este ejemplo 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
        }
    }
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.