Java Web 应用如何过滤特殊字符防止存储型 XSS 攻击?

文章导读
防止存储型 XSS 不能只靠输入端过滤特殊字符,最可靠的做法是在数据输出到页面时进行上下文相关的编码处理。标题虽提及“过滤”,但工程实践中“输出编码”才是防御核心,输入过滤仅作为辅助手段。
📋 目录
  1. A 核心原则:为什么输入过滤不够
  2. B Spring Boot 安全配置实战
  3. C 手动编码实战(OWASP Encoder)
  4. D 怎么验证是否生效
  5. E 常见坑
  6. F 参考来源
A A

防止存储型 XSS 不能只靠输入端过滤特殊字符,最可靠的做法是在数据输出到页面时进行上下文相关的编码处理。标题虽提及“过滤”,但工程实践中“输出编码”才是防御核心,输入过滤仅作为辅助手段。

先说结论:输入过滤只能作为辅助手段,核心防御必须在输出阶段根据 HTML、JS、URL 等不同上下文进行编码。

  • 先判断:确认数据输出位置是 HTML body、属性还是 JavaScript 变量
  • 优先做:引入 OWASP Java Encoder 或启用 Spring Security 默认转义
  • 再验证:使用 XSS 测试 payload 确认脚本未被执行

核心原则:为什么输入过滤不够

存储型 XSS 的本质是浏览器信任了服务端返回的内容。攻击者将恶意脚本存入数据库,当其他用户访问页面时,服务端从数据库取出数据直接拼接到 HTML 中返回,浏览器将其当作正常代码执行。

单纯在输入端过滤特殊字符(如禁用 <script>)往往不够,因为攻击者可以利用事件 handler(如 onerror)、JavaScript 协议头或编码绕过。只有在输出端将特殊字符转换为 HTML 实体(如 < 转为 &lt;),浏览器才会将其视为纯文本显示而非代码执行。

Spring Boot 安全配置实战

如果你使用 Spring Boot 2.7+ 或 3.x,请使用 Java Config 配置安全头。注意:X-XSS-Protection 头已被现代浏览器弃用,不建议再配置,应优先使用 Content-Security-Policy (CSP)。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.headers(headers -> headers
        // 配置 CSP,限制脚本执行来源
        .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'; script-src 'self'"))
        // 防止点击劫持
        .frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
        // 防止 MIME 类型嗅探
        .contentTypeOptions(Customizer.withDefaults())
    );
    // 注意:不要启用 xssProtection(),该功能已废弃
    return http.build();
}

配置完成后,确保项目运行在 HTTPS 环境下,否则部分安全头(如 HSTS)可能无法生效或被中间人篡改。

手动编码实战(OWASP Encoder)

对于直接在 Servlet 输出流、复杂拼接场景或框架默认转义未覆盖的地方,建议使用 OWASP Java Encoder 项目。它比传统的 HTML 转义更安全,能区分 HTML、JS、URL 等不同上下文。

1. 引入依赖

<!-- Maven 依赖,建议检查 Maven Central 获取最新版本 -->
<dependency>
    <groupId>org.owasp.encoder</groupId>
    <artifactId>encoder</artifactId>
    <version>1.2.3</version>
</dependency>

2. Controller 层调用示例

Java Web 应用如何过滤特殊字符防止存储型 XSS 攻击?

在数据返回给视图前进行编码,而不是在存入数据库时编码(避免破坏数据)。

@GetMapping("/profile")
public String profile(Model model, @RequestParam String userInput) {
    // 根据输出上下文选择编码方法
    String safeHtml = Encode.forHtml(userInput);
    String safeJs = Encode.forJavaScript(userInput);
    
    model.addAttribute("safeInput", safeHtml);
    return "user/profile";
}

怎么验证是否生效

1. 页面源码检查

在浏览器右键查看页面源代码,搜索你输入的测试字符。如果看到 <script> 变成了 &lt;script&gt;,说明 HTML 实体编码已生效。

2. 控制台报错观察

尝试输入 <img src=x onerror=alert(1)>。如果编码生效,图片不会加载,控制台也不会弹出 alert 窗口。如果弹出窗口,说明防御失败。

3. 自动化扫描

使用 OWASP ZAP 或 Burp Suite 的 XSS 扫描插件对提交接口进行测试,查看是否有高危漏洞报告。

Java Web 应用如何过滤特殊字符防止存储型 XSS 攻击?

常见坑

1. JSON 接口未处理

如果是前后端分离架构,后端返回 JSON 数据,前端使用 innerHTML 渲染,后端转义可能无效。需确保前端使用 textContent 或框架提供的安全绑定方式(如 Vue 的 {{ }} 而非 v-html)。

2. 富文本编辑器场景

如果业务允许用户发布含格式的文章(如博客),不能简单转义所有标签。需使用白名单过滤库(如 OWASP Java HTML Sanitizer),只保留安全的 HTML 标签和属性。

3. 双重编码问题

避免对同一数据进行多次编码,否则页面会显示乱码(如 &amp;lt;)。确认框架是否已经自动转义,不要手动重复处理。

4. 安全头依赖 HTTPS

配置了 CSP 或 HSTS 但未强制 HTTPS,攻击者仍可通过 HTTP 劫持响应头。建议在网关或负载均衡层强制跳转 HTTPS。

参考来源

  • OWASP Cross Site Scripting Prevention Cheat Sheet, https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
  • OWASP Java Encoder Project, https://owasp.org/www-project-java-encoder/
  • Spring Security Documentation, https://docs.spring.io/spring-security/reference/servlet/headers.html