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


114

现有的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'etx等API。在哪里可以得到帮助?
sskumar86 '16

Answers:


132

如您所知 HttpServletRequest,没有setParameter方法。这是故意的,因为该类表示来自客户端的请求,而修改参数将不表示该请求。

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

这有点丑陋,但这就是servlet API所说的。如果您尝试将其他任何内容传递给doFilter,则某些servlet容器会抱怨您违反了规范,并拒绝处理它。

一个更好的解决方案是更多的工作-修改处理该参数的原始Servlet / JSP,以便它期望使用请求属性而不是参数。过滤器检查参数,将其净化,然后使用request.setAttribute净化值设置属性(使用)。没有子类,没有欺骗,但确实需要您修改应用程序的其他部分。


6
HttpServletRequestWrapper很棒;我不知道它的存在。谢谢!
杰里米·斯坦

3
感谢您选择其他属性!在Head First Servlet和JSP中使用请求和响应包装器看到了示例代码,并且不敢相信该规范会促使人们采用这种方式。
凯文

我已经接触了控制器中的值,并设置了tha参数(通过电子邮件发送)...现在如何替换servlet中的值 <property name="username" value="somemail@gmail.com" /> //Change email on logging in <property name="password" value="*********" />//Change Password on logging in
UmaShankar 2015年

73

作为记录,这是我最后写的课:

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
您可能还需要考虑getParameterMap方法。也许抛出和不受支持的异常,以便没有组件使用该方法并跳过清理逻辑。
汤姆

1
好点,汤姆。在这种情况下,我检查并发现没有被调用,但为了完整性和下一个人的缘故,我应该添加该名称。谢谢!
杰里米·斯坦

13
看来我是下一位,杰里米。我在寻找选项来修改从外部应用程序传递到第三方Servlet的数据时找到了这篇文章。在我的情况下,该servlet并未调用HTTPServletRequest.getParameter(),getParameterMap()甚至getAttribute()来获取请求数据,因此,通过反复试验,我确定该Servlet正在调用HTTPServletRequest.getInputStream()。和getQueryString()。对于任何尝试关闭servlet的人,我的建议是将每个访问器都包装在HTTPServletRequest中,以了解实际情况
Fred Sobotka

3
对于SrpingMVC,您将需要重写getParameterValues()来愚弄Spring。RequestParamMethodArgumentResolver.resovleName()使用该方法,因此您将获得MissingServletRequestParameterException而不会被覆盖。在带有Spring-web 4.1.7的Spring Boot 1.2.6上进行了测试。
barryku 2015年

10

编写一个简单的类,该类HttpServletRequestWrapper使用getParameter()方法进行归类,该方法返回输入的已净化版本。然后将您的实例传递HttpServletRequestWrapper给,Filter.doChain()而不是直接传递请求对象。


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请求。由于这是池:

    1. 该线程池中的一个线程将处理许多HTTP请求,但一次只能处理一个(因此,您需要在使用后清理变量或为每个HTTP请求定义变量=注意代码,例如,if (value!=null) { THREAD_VARIABLE.set(value);}因为您将重用该值从前一个HTTP请求返回valuenull 时(保证有副作用)。
    2. 无法保证两个请求将由同一线程处理(可能是这种情况,但您不能保证)。如果您需要将用户数据从一个请求转移到另一个请求,则最好使用HttpSession.setAttribute()
  • JEE在@RequestScoped内部使用ThreadLocal,但是使用ThreadLocal更通用:您可以在非JEE / CDI容器中使用它(例如,在多线程JRE应用程序中)

在线程范围内设置参数真的是一个好主意吗?多个请求会看到同一线程吗?(我认为不是)
Zachary Craig

是个好主意吗=是(但是您需要知道您在做什么,无论如何JEE @RequestScoped在内部都一样)。多个请求将看到同一个线程=否(或者至少您不能保证)。我已经编辑了答案以精确说明这些问题。
朱利安·克朗格

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

1

根据您的所有评论,以下是对我有用的建议:

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

    }

注意:queryString()要求处理每个KEY的所有值,并且在添加自己的param值时,如果需要的话,不要忘了encodeUrl()

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


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

-1

尝试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
        }
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.