【问题标题】:Modify request parameter with servlet filter使用 servlet 过滤器修改请求参数
【发布时间】:2010-11-27 15:22:46
【问题描述】:

现有的 Web 应用程序正在 Tomcat 4.1 上运行。页面存在 XSS 问题,但我无法修改源。我决定编写一个 servlet 过滤器来在页面看到参数之前对其进行清理。

我想写一个这样的过滤器类:

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。帮忙?

标签: java servlet-filters


【解决方案1】:

正如您所指出的,HttpServletRequest 没有 setParameter 方法。这是故意的,因为类代表来自客户端的请求,而修改参数不会代表这一点。

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

这有点难看,但 servlet API 说你应该这样做。如果您尝试将其他任何内容传递给doFilter,一些 servlet 容器会抱怨您违反了规范,并会拒绝处理。

一个更优雅的解决方案是更多的工作 - 修改处理参数的原始 servlet/JSP,使其期望请求 attribute 而不是参数。过滤器检查参数,对其进行清理,并使用清理后的值设置属性(使用request.setAttribute)。没有子类化,没有欺骗,但确实需要您修改应用程序的其他部分。

【讨论】:

  • HttpServletRequestWrapper 很棒;我从来不知道它的存在。谢谢!
  • 感谢属性设置选项!在 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 中的值
【解决方案2】:

为了记录,这是我最终写的课程:

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

【讨论】:

  • 您可能还需要考虑 getParameterMap 方法。可能会抛出和不受支持的异常,因此没有组件使用该方法并跳过清理逻辑。
  • 好点,汤姆。在这种特殊情况下,我检查并发现它没有被调用,但为了完整性和下一个人的缘故,我应该添加它。谢谢!
  • 看来我就是那个下一个人,杰里米。我在寻找修改从外部应用程序传递到第三方 servlet 的数据的选项时发现了这篇文章。在我的例子中,servlet 没有调用 HTTPServletRequest.getParameter()、getParameterMap() 甚至 getAttribute() 来获取请求数据,因此,通过反复试验,我确定 servlet 正在调用 HTTPServletRequest.getInputStream()和 getQueryString()。我对任何尝试对封闭 servlet 执行此任务的人的建议是将每个访问器包装在 HTTPServletRequest 中,以便了解真正发生的事情
  • 对于 SrpingMVC,你需要重写 getParameterValues() 来欺骗 Spring。 RequestParamMethodArgumentResolver.resovleName() 使用该方法,因此您将获得 MissingServletRequestParameterException 而无需覆盖。使用 spring-web 4.1.7 在 Spring Boot 1.2.6 上测试。
【解决方案3】:

编写一个简单的类,使用 getParameter() 方法对HttpServletRequestWrapper 进行子类化,该方法返回输入的净化版本。然后将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://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 的所有值,如果需要,在添加自己的参数值时不要忘记 encodeUrl()

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

    【讨论】:

      【解决方案5】:

      我遇到了同样的问题(更改过滤器中 HTTP 请求的参数)。我最终使用了ThreadLocal&lt;String&gt;。在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);} 之类的代码因为当 value 为 null 时,您将重用上一个 HTTP 请求中的值:保证副作用)。
        2. 无法保证两个请求将由同一个线程处理(可能是这种情况,但您无法保证)。如果您需要将用户数据从一个请求保留到另一个请求,最好使用HttpSession.setAttribute()
      • JEE @RequestScoped 在内部使用 ThreadLocal,但使用 ThreadLocal 更加通用:您可以在非 JEE/CDI 容器中使用它(例如在多线程 JRE 应用程序中)

      【讨论】:

      • 在线程范围内设置参数真的是个好主意吗?多个请求会看到同一个线程吗? (我假设不是)
      • 这是一个好主意 = 是的(但你需要知道你在做什么,无论如何 JEE @RequestScoped 在内部也是一样的)。多个请求会看到同一个线程=否(或者至少你不能保证)。我已经编辑了答案以精确说明这些要点。
      【解决方案6】:

      这就是我最终做的事情

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

      【讨论】:

        【解决方案7】:

        您可以使用正则表达式进行清理。在调用 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();   
            }
        }
        

        【讨论】:

        • 你不要这样修改原始请求参数,而是在副本上。
        【解决方案8】:

        试试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
                }
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-11-19
          • 2018-12-30
          • 2013-11-29
          • 2016-12-08
          • 2012-02-29
          • 2013-04-12
          • 2018-12-28
          • 2011-12-18
          相关资源
          最近更新 更多