使用Servlet过滤器修改请求参数

131

一个已有的web应用正在Tomcat 4.1上运行。其中有一个页面存在XSS漏洞,但我无法修改源代码。因此,我决定编写一个Servlet过滤器,在参数传递给页面之前对其进行处理。

我想要编写这样一个Filter类:

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

但是 ServletRequest.setParameter 方法不存在。

在将请求传递给下一个处理程序之前,我该如何更改请求参数的值?


HttpServletRequestWrapper有很多API定义。我正在努力理解每个API的含义。Javadoc无法帮助我理解像'userinRole','getPrincipal'等API。我应该在哪里寻求帮助? - sskumar86
8个回答

150

如您所述,HttpServletRequest 没有 setParameter 方法。这是故意的,因为该类表示从客户端发送的请求,并修改参数将不代表它。

一种解决方案是使用 HttpServletRequestWrapper 类,它允许您使用另一个请求来包装一个请求。您可以对其进行子类化,并覆盖 getParameter 方法以返回经过消毒处理的值。然后,您可以将该包装请求传递给 chain.doFilter 而不是原始请求。

这有点丑陋,但这就是servlet API要求您做的事情。如果您试图传递其他内容给 doFilter,一些servlet容器将抱怨您违反了规范,并将拒绝处理它。

更优雅的解决方案需要更多的工作-修改处理参数的原始servlet/JSP,使其期望请求属性而不是参数。过滤器检查参数,消毒它,并使用request.setAttribute设置属性为经过消毒处理的值。无需子类化,无需欺骗,但需要您修改应用程序的其他部分。


7
HttpServletRequestWrapper很棒,我以前不知道它的存在。谢谢! - Jeremy Stein
3
感谢提供属性设置的替代方案!在《Head First Servlets 和 JSP》中看到使用请求和响应包装器的示例代码,无法相信规范要求人们以那种方式处理事情。 - Kevin
我已经在控制器中设置了我的值,并设置了参数(电子邮件和密码)...现在我该如何在我的servlet中替换这些值?<property name="username" value="somemail@gmail.com" /> //更改登录时的电子邮件 <property name="password" value="*********" />//更改登录时的密码 - UmaShankar

82

记录一下,这是我最终编写的类:

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

6
你可能还需要考虑 getParameterMap 方法。也许可以抛出一个不受支持的异常,以便没有组件使用该方法并跳过清理逻辑。 - Tom
1
很好的观点,汤姆。在这种特殊情况下,我检查了一下发现它没有被调用,但是为了完整性和下一个人的利益,我应该添加上去。谢谢! - Jeremy Stein
16
看起来我是下一个人,Jeremy。我在寻找修改从外部应用程序传递给第三方servlet的数据选项时发现了这篇文章。在我的情况下,该servlet没有调用HTTPServletRequest.getParameter()、 getParameterMap()或者getAttribute()来获取请求数据,所以通过试错,我确定该servlet正在调用HTTPServletRequest.getInputStream()和getQueryString()。我对于任何试图针对封闭servlet执行此任务的人的建议是,将HTTPServletRequest中的每个访问器都包装起来,以便了解实际发生的情况。 - Fred Sobotka
3
针对SpringMVC,您需要覆盖getParameterValues()方法来欺骗Spring。RequestParamMethodArgumentResolver.resolveName()方法使用该方法,所以如果不进行覆盖,则会出现MissingServletRequestParameterException错误。在使用spring-web 4.1.7和Spring Boot 1.2.6进行测试。 - barryku

11

编写一个简单的类,它继承HttpServletRequestWrapper,并实现一个getParameter()方法,该方法返回输入的经过消毒处理后的版本。然后,将您的HttpServletRequestWrapper实例传递给Filter.doChain(),而不是直接传递请求对象。


4

根据您在此处的所有评论,这是我为我自己提出的方案:

 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://dev59.com/SHE85IYBdhLWcg3wXCW1#35831692
            // 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");
        }

    }

注意:queryString()需要处理每个KEY的所有值,并在添加自己的参数值时(如有必要)不要忘记使用encodeUrl()进行编码。

作为限制条件,如果您调用request.getParameterMap()或任何会调用request.getReader()并开始读取的方法,则将防止进一步调用request.setCharacterEncoding(...)。


1

我曾经遇到相同的问题(在过滤器中更改HTTP请求的参数)。最终我使用了一个ThreadLocal<String>。在Filter中,我有:

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

在我的请求处理器(HttpServlet,JSF控制器或任何其他HTTP请求处理器)中,我会获取当前线程的值:
...
String myVariable = MyFilter.THREAD_VARIABLE.get();
...

优点:

  • 比传递HTTP参数更加灵活(您可以传递POJO对象)
  • 稍微快一些(无需解析URL以提取变量值)
  • HttpServletRequestWrapper样板更加优雅
  • 变量范围比仅限于HTTP请求更广泛(进行request.setAttribute(String,Object)时的作用域,即您可以在其他过滤器中访问变量。

缺点:

  • 只有处理过滤器的线程与处理HTTP请求的线程相同时才能使用此方法(这是我所知道的所有基于Java的服务器的情况)。因此,在以下情况下将不起作用
    • 执行HTTP重定向(因为浏览器会执行新的HTTP请求,并且无法保证它将由同一线程处理)
    • 在单独的线程中处理数据,例如使用java.util.stream.Stream.paralleljava.util.concurrent.Futurejava.lang.Thread
  • 必须能够修改请求处理器/应用程序

一些注意事项:

服务器有一个线程池来处理HTTP请求。由于这是一个线程池:一个线程从这个线程池中将会处理许多HTTP请求,但一次只能处理一个(因此您需要在使用后清理变量或为每个HTTP请求定义变量 = 注意代码,如 if (value!=null) { THREAD_VARIABLE.set(value);} 因为当value为空时,您将重用上一个HTTP请求的值:副作用是保证的)。没有保证两个请求将由同一个线程处理(可能是这种情况,但您没有保证)。如果您需要从一个请求中保留用户数据到另一个请求,最好使用 HttpSession.setAttribute()。JEE @RequestScoped 内部使用ThreadLocal,但使用ThreadLocal更加灵活:您可以在非JEE/CDI容器中使用它(例如在多线程JRE应用程序中)。

在线程范围内设置参数真的是个好主意吗?多个请求会看到同一个线程吗?(我认为不会) - Zachary Craig
这是个好主意 = 是的(但你需要知道你在做什么,无论如何JEE的@RequestScoped内部执行相同操作)。多个请求是否会看到相同的线程 = 不会(或者至少你没有保证)。我已经编辑了答案以澄清这些要点。 - Julien Kronegg

1

这就是我最终做的事情

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

0

您可以使用正则表达式进行净化。在调用chain.doFilter(request, response)方法之前,在过滤器内部调用此代码。 以下是示例代码:

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
您不应该直接修改原始请求参数,而是应该在副本上进行修改。 - Mehdi

-3
尝试使用request.setAttribute("param",value);。这对我来说很有效。
请查找此代码示例:
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
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接