Spring MVC请求参数被全局拦截器篡改的排查与规范方案

Spring MVC 请求参数全局拦截器篡改的排查与规范方案

大家好,今天我们来聊聊 Spring MVC 中请求参数被全局拦截器篡改的问题。这是一个在实际开发中容易遇到,但又比较隐蔽的 bug。处理不当,会导致数据安全隐患,业务逻辑混乱,甚至造成难以追踪的错误。

一、问题场景描述

在 Spring MVC 应用中,我们经常使用拦截器(Interceptor)来处理一些通用的请求逻辑,例如:

  • 权限校验
  • 日志记录
  • 统一参数处理
  • 防止 XSS 攻击

通常,我们会继承 HandlerInterceptor 接口,并实现 preHandlepostHandleafterCompletion 方法。问题往往出现在 preHandle 方法中,因为这个方法在请求到达 Controller 之前执行,有机会修改请求参数。

假设我们有一个需求:为了防止恶意用户提交包含 HTML 标签的数据,我们需要在请求到达 Controller 之前,对所有 String 类型的参数进行 HTML 编码。于是我们编写了一个拦截器:

import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;

public class XSSFilterInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Enumeration<String> parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            String parameterName = parameterNames.nextElement();
            String[] parameterValues = request.getParameterValues(parameterName);
            if (parameterValues != null) {
                for (int i = 0; i < parameterValues.length; i++) {
                    if (parameterValues[i] instanceof String) {
                        parameterValues[i] = cleanXSS(parameterValues[i]); // 使用自定义的cleanXSS方法进行HTML编码
                        //关键代码:直接修改 request 的 parameterValues
                        request.setAttribute(parameterName, parameterValues[i]);
                    }
                }
            }
        }
        return true;
    }

    private String cleanXSS(String value) {
        // 这里是 HTML 编码的实现,为了简洁,省略具体代码
        return org.springframework.web.util.HtmlUtils.htmlEscape(value);
    }
}

并将拦截器配置到 Spring MVC 中:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new XSSFilterInterceptor())
                .addPathPatterns("/**"); // 拦截所有请求
    }
}

看起来一切都很完美,所有请求都会经过 XSS 过滤。但是,这种做法存在一个严重的问题:直接修改 request.setAttribute 中的参数值,可能会导致 Controller 中获取到的参数与用户实际提交的参数不一致。尤其是在使用 ModelAttribute 注解或者通过反射获取参数值的情况下,更容易出现问题。

二、问题排查方法

当出现请求参数被篡改的现象时,我们可以按照以下步骤进行排查:

  1. 确认问题范围: 首先,确认问题是否只出现在特定的 Controller 或者特定的请求中。如果是,可以缩小排查范围,重点关注相关的拦截器和 Controller 代码。

  2. 禁用拦截器: 临时禁用所有拦截器,观察问题是否仍然存在。如果问题消失,说明问题确实是由某个拦截器引起的。

  3. 逐个启用拦截器: 逐个启用拦截器,每次启用一个,观察问题是否出现。这样可以快速定位到导致问题的拦截器。

  4. 断点调试: 在拦截器的 preHandle 方法中设置断点,观察请求参数的值在拦截器执行前后是否发生了变化。

  5. 日志记录: 在拦截器的 preHandle 方法中添加日志记录,记录请求参数的值,方便后续分析。

  6. 对比参数获取方式: 在Controller中,使用不同的方式获取参数(例如:@RequestParam@ModelAttributerequest.getParameter())并打印出来,对比它们的值是否一致。这有助于判断是哪种参数获取方式受到了拦截器的影响。

代码示例:Controller参数获取对比

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;

@Controller
public class TestController {

    @GetMapping("/test")
    @ResponseBody
    public String test(
            @RequestParam("name") String name,
            @ModelAttribute("user") User user,
            HttpServletRequest request
    ) {
        System.out.println("@RequestParam: " + name);
        System.out.println("@ModelAttribute: " + user.getName());
        System.out.println("request.getParameter("name"): " + request.getParameter("name"));
        return "OK";
    }

    //ModelAttribute 对应的实体类
    @ModelAttribute("user")
    public User getUser(@RequestParam("name") String name) {
      User user = new User();
      user.setName(name);
      return user;
    }

}

class User {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

表格总结排查步骤:

步骤 操作 预期结果
1 确认问题范围 缩小排查范围,确定受影响的 Controller 和请求
2 禁用所有拦截器 问题消失,确认是由拦截器引起
3 逐个启用拦截器 定位到导致问题的拦截器
4 断点调试拦截器 preHandle 方法 观察参数值变化
5 添加日志记录 方便后续分析
6 对比Controller参数获取方式 确定哪种参数获取方式受到影响

三、规范方案

为了避免请求参数被篡改的问题,我们需要采取一些规范措施。

  1. 避免直接修改 Request 参数: 拦截器的主要职责是对请求进行预处理,而不是直接修改请求参数。如果需要修改参数,应该使用更安全的方式。

  2. 使用 Request Wrapper: 可以创建一个 HttpServletRequestWrapper,在 Wrapper 中对参数进行修改,然后将 Wrapper 传递给后续的 Handler。这样可以保证原始的 Request 对象不被修改。

代码示例:使用 Request Wrapper

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.HashMap;
import java.util.Map;

public class XSSRequestWrapper extends HttpServletRequestWrapper {

    private Map<String, String[]> filteredParams = new HashMap<>();

    public XSSRequestWrapper(HttpServletRequest request) {
        super(request);
        // 预先处理所有参数
        Map<String, String[]> originalParams = request.getParameterMap();
        for (String paramName : originalParams.keySet()) {
            String[] originalValues = originalParams.get(paramName);
            String[] filteredValues = new String[originalValues.length];
            for (int i = 0; i < originalValues.length; i++) {
                filteredValues[i] = cleanXSS(originalValues[i]);
            }
            filteredParams.put(paramName, filteredValues);
        }

    }

    @Override
    public String getParameter(String name) {
        String[] values = filteredParams.get(name);
        if (values != null && values.length > 0) {
            return values[0];
        }
        return null;
    }

    @Override
    public String[] getParameterValues(String name) {
        return filteredParams.get(name);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return filteredParams;
    }

    private String cleanXSS(String value) {
        // 这里是 HTML 编码的实现,为了简洁,省略具体代码
        return org.springframework.web.util.HtmlUtils.htmlEscape(value);
    }
}

修改拦截器,使用 Request Wrapper:

import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class XSSFilterInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        XSSRequestWrapper xssRequest = new XSSRequestWrapper(request);
        // 将包装后的 request 传递给后续的 Handler
        return true;
    }
}

注意:

  • 需要配置 Filter 来使用 XSSRequestWrapper,因为拦截器无法直接修改 request 对象。
  • Filter需要在拦截器之前执行,以确保拦截器获取到的是包装后的请求对象。

Filter配置示例:

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<Filter> xssFilterRegistrationBean() {
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new XSSFilter());
        registration.addUrlPatterns("/*");
        registration.setName("xssFilter");
        registration.setOrder(1); // 设置优先级,确保Filter在拦截器之前执行
        return registration;
    }
}

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class XSSFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        XSSRequestWrapper xssRequest = new XSSRequestWrapper((HttpServletRequest) request);
        chain.doFilter(xssRequest, response);
    }
}
  1. 使用 AOP: 如果只需要对特定的 Controller 方法进行参数处理,可以使用 AOP,在方法执行前后对参数进行修改。

  2. 统一参数处理类: 可以创建一个专门的参数处理类,负责对所有请求参数进行统一处理。然后在 Controller 中调用这个类的方法来获取参数,避免直接从 Request 对象中获取。

  3. 自定义参数解析器: Spring MVC 提供了自定义参数解析器的机制,可以通过实现 HandlerMethodArgumentResolver 接口,自定义参数解析逻辑。这样可以更好地控制参数的解析过程,避免参数被篡改。

代码示例:自定义参数解析器

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class XSSStringArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(String.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String parameterName = parameter.getParameterName();
        String parameterValue = webRequest.getParameter(parameterName);
        if (parameterValue != null) {
            return cleanXSS(parameterValue);
        }
        return null;
    }

    private String cleanXSS(String value) {
        // 这里是 HTML 编码的实现,为了简洁,省略具体代码
        return org.springframework.web.util.HtmlUtils.htmlEscape(value);
    }
}

配置自定义参数解析器:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new XSSStringArgumentResolver());
    }
}

表格总结规范方案:

方案 优点 缺点 适用场景
Request Wrapper 避免直接修改 Request 对象,安全可靠 需要配置 Filter,实现较为复杂 需要对所有请求参数进行统一处理
AOP 灵活,只对特定方法生效 代码侵入性较强 只需要对特定的 Controller 方法进行参数处理
统一参数处理类 代码复用性高 需要手动调用,容易遗漏 需要对多个 Controller 方法进行相同的参数处理
自定义参数解析器 Spring MVC 原生支持,控制参数解析过程 实现较为复杂,需要了解 Spring MVC 的参数解析机制 需要对特定类型的参数进行特殊处理

四、最佳实践

在实际开发中,建议采用以下最佳实践:

  1. 明确拦截器的职责: 拦截器的主要职责是对请求进行预处理,例如权限校验、日志记录等。尽量避免在拦截器中修改请求参数。

  2. 选择合适的参数处理方案: 根据实际需求,选择合适的参数处理方案。如果需要对所有请求参数进行统一处理,可以使用 Request Wrapper。如果只需要对特定的 Controller 方法进行参数处理,可以使用 AOP 或者自定义参数解析器。

  3. 编写单元测试: 针对拦截器和参数处理逻辑编写单元测试,确保代码的正确性和稳定性。

  4. 代码审查: 定期进行代码审查,及时发现潜在的问题。

  5. 记录操作日志: 记录用户的操作日志,方便问题排查和安全审计。

五、案例分析

假设一个电商网站,用户可以提交商品评论。为了防止恶意用户提交包含 JavaScript 代码的评论,我们需要对评论内容进行 XSS 过滤。

  • 错误的做法: 在拦截器中直接修改 request.setAttribute 中的评论内容。
  • 正确的做法: 使用 Request Wrapper,在 Wrapper 中对评论内容进行 HTML 编码,然后将 Wrapper 传递给 Controller。或者使用自定义参数解析器,在解析评论内容时进行 HTML 编码。

六、总结与建议

总而言之,在 Spring MVC 中,全局拦截器篡改请求参数是一个需要重视的问题。通过合理的排查方法和规范方案,我们可以有效地避免这个问题,提高应用程序的安全性和稳定性。关键在于:

  • 理解拦截器的职责,避免过度干预请求参数。
  • 选择合适的参数处理方案,根据实际需求进行权衡。
  • 加强代码审查和单元测试,确保代码质量。

希望今天的分享对大家有所帮助。谢谢!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注