Spring Boot 项目接口 XSS 漏洞处理

起因

项目被扫描了一个 XSS 漏洞风险,接口代码如下:

@RequestMapping("/redirect")
public void sendRedirect(HttpServletResponse response,
        @RequestParam(name = SPRING_SECURITY_FORM_USERNAME_KEY) String userName,
        @RequestParam(name = SPRING_SECURITY_FORM_PASSWORD_KEY) String password,
        @RequestParam(name = LOGIN_URL) String redirectUrl,
        HttpServletRequest request) throws IOException {
   //...
}

这个接口接收了 userName, password, redirectUrl 三个参数用于登录重定向。即用户可以在参数中注入恶意代码,从而实现跨站脚本攻击

(XSS)。

例如 构造以下攻击 URL,就会执行对应的 Script 。

.../redirect?redirect_url=&password=&username="><script>alert( "xss")</script>

XSS

XSS 漏洞是指攻击者通过在网页中注入恶意代码,使得用户在访问网页时执行这些恶意代码,从而达到攻击的目的。XSS 攻击主要有以下两种形式:

  • 存储型 XSS 攻击:攻击者将恶意代码存储到数据库中,并在网页中显示出来。当用户访问这个网页时,恶意代码就会被执行。
  • 反射型 XSS 攻击:攻击者将恶意代码作为参数传递
  • 给网页,网页在响应时将这些参数输出到页面上。当用户访问这个网页时,恶意代码就会被执行。

XSS 攻击的危害非常大,攻击者可以通过 XSS 攻击偷取用户的敏感信息(如用户名、密码、银行卡号等)、篡改网页内容、重定向用户到恶意网站等。

修复

  • 使用 Spring 框架提供的 HtmlUtils 类对特殊字符进行转义处理,从而防止注入攻击。示例代码如下:
@RequestMapping("/redirect")
public void sendRedirect(HttpServletResponse response,
        @RequestParam(name = SPRING_SECURITY_FORM_USERNAME_KEY) String userName,
        @RequestParam(name = SPRING_SECURITY_FORM_PASSWORD_KEY) String password,
        @RequestParam(name = LOGIN_URL) String redirectUrl,
        HttpServletRequest request) throws IOException {
  // 使用 HtmlUtils 过滤特殊字符
    String userName = HtmlUtils.htmlEscape(userName);
    String password = HtmlUtils.htmlEscape(password);
    String redirectUrl = HtmlUtils.htmlEscape(redirectUrl);
  // ...
}
  • 使用 Jsoup 库过滤 HTML 标签,保留需要的标签并删除不必要的标签。Whitelist.none(); 对所有输入参数进行了白名单过滤,这意味着不允许任何 HTML 标签和属性。如果需要允许某些标签和属性,请使用更具体的白名单。
  // 对所有输入参数使用Jsoup进行白名单过滤
    userName = Jsoup.clean(userName, Whitelist.none());
    password = Jsoup.clean(password, Whitelist.none());
  redirectUrl = Jsoup.clean(redirectUrl, Whitelist.none());
  // ...

扩展

装饰者模式

(HttpServletRequestWrapper) + 过滤器 (Filter)

装饰者模式定义: 动态将责任附加到对象上。想要扩展功能,装饰者提供有别于继承的另一种选择。

装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。

新建 ParameterRequestWrapper继承 HttpServletRequestWrapper ,并且在 getParameter()和getParameterValues()方法中对参数通过 HtmlUtils.htmlEscape() 进行了过滤转义。

public class ParameterRequestWrapper extends HttpServletRequestWrapper {
    public ParameterRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        if (value == null) {
            return null;
        }
        return HtmlUtils.htmlEscape(value, "UTF-8");
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) {
            return null;
        }
        int length = values.length;
        String[] escapseValues = new String[length];
        for (int i = 0; i < length; i++) {
            escapseValues[i] = HtmlUtils.htmlEscape(values[i], "UTF-8");
        }
        return escapseValues;
    }

    @Override
    public Enumeration<String> getParameterNames() {
        Enumeration<String> enumeration = super.getParameterNames();
        List<String> list = new ArrayList<String>();
        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            list.add(name);
        }
        return Collections.enumeration(list);
    }
}

新建一个名为 xssFilter的过滤器,并将它应用到所有的URL上。在 doFilter() 方法中,我们使用上面创建的 ParameterRequestWrapper对请求进行包装。

@WebFilter(filterName = "xssFilter", urlPatterns = "/*")
public class XssFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化操作
    }

    @Override
    public void destroy() {
        // 销毁操作
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 对参数进行过滤
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        ParameterRequestWrapper wrapper = new ParameterRequestWrapper(httpRequest);
        chain.doFilter(wrapper, response);
    }

}

需要注意的是,这种方式有可能会对一些合法的输入进行误判,例如用户希望输入一些特殊字符或者HTML标签。故有需求时还需根据实际自行更改其中的实现。

拦截器

(Intercepter)

拦截器的实现与过滤器类似,不同的是拦截器可以更加细粒度地控制请求的处理过程。

首先,创建一个拦截器类

,该类需要实现HandlerInterceptor接口,并重写preHandle() 方法。preHandle() 方法在请求到达目标 Controller 之前被调用,可以在此方法中进行参数过滤。

public class XssInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<String, String[]> parameterMap = request.getParameterMap();
        for (String key : parameterMap.keySet()) {
            String[] values = parameterMap.get(key);
            for (int i = 0; i < values.length; i++) {
                values[i] = HtmlUtils.htmlEscape(values[i], "UTF-8");
            }
            request.getParameterMap().put(key, values);
        }
        return true;
    }
}

} 然后,需要在 Spring Boot 应用程序中注册该拦截器。可以通过在配置类中添加 @Bean 注解来创建拦截器实例,并通过addInterceptors() 方法将拦截器添加到WebMvcConfigurer中。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Bean
    public XssInterceptor xssInterceptor() {
        return new XssInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(xssInterceptor()).addPathPatterns("/**");
    }
}

JSON 反序列化器

(适用于 JSON 形式传参)

前后端分离场景下,基本都是通过 @RequestBody 注解接收 application/json 格式的请求体,所以上述 装饰者模式+过滤器 方式不再适用。我们可以直接定义一个全局的 JSON 反序列化器来进行处理。

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java