flawopen.com/xss/Java
●
CWE-79 · 高危
安全研究 · FlawOpen
Java XSS:保护 Spring MVC 与 Thymeleaf 输出
th:utext 和未转义的 JSP 输出可能执行注入的 JavaScript。按输出上下文编码,并用允许列表清理富文本(CWE-79)。
💡 通俗易懂的原理解析 (ELI5)
体育场记分牌有普通姓名字段,也有单独的富文本显示模式。Thymeleaf 的 th:text 会把提交的姓名当作普通文本;th:utext 则要求渲染器把该值当作记分牌的 HTML 指令。浏览器收到体育场网站的 text/html 后,会以该网站的权限渲染标记。用户文本应使用转义输出;确实需要富文本时,先清理内容再放入不转义的模板槽位。
核心概念与专有名词
Thymeleaf th:text vs th:utextth:text会对值做 HTML 转义;th:utext(未转义文本)则把它作为标记插入。在 Thymeleaf 的内联写法中,[[${x}]]会转义,[(${x})]不会。绑定到用户输入的th:utext就是 XSS,除非该值事先经过净化。JSP ${} vs <c:out>- 在 JSP 模板文本中,
${param.q}会不经转义直接输出。<c:out value="${param.q}"/>或${fn:escapeXml(param.q)}会转义它。<%= request.getParameter("q") %>这类脚本片段从不转义。 Encode.forHtml / forJavaScript- OWASP Java Encoder 为每种输出上下文提供一个方法:
Encode.forHtml用于元素内容,forHtmlAttribute用于带引号的属性,forJavaScript用于 JavaScript 字符串字面量,forUriComponent用于 URL 参数。放进脚本里的值只做 HTML 编码是不够的。 OWASP Java HTML Sanitizer- 对于用户提供的 HTML,
Sanitizers.FORMATTING.and(Sanitizers.LINKS)这样的策略会维护标签和属性白名单,删除脚本、事件处理器和javascript:URL。编码会把标签显示成文本;净化则让安全的标签继续生效。 @ResponseBody returning text/html@RestController中声明produces = text/html并返回String的方法,其返回值会原样写入响应:不会运行任何模板引擎,所以什么都不会被转义。应优先使用视图模板,或对每个拼接进去的值做编码。
框架映射
- 本指南所述弱点的 CWE 参考: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
攻击执行流程分解
步骤 1
输入随请求参数到达
Spring 把查询字符串或表单中的 q、user 或 bio 绑定到 String 参数。攻击者可以给受害者发送精心构造的链接,或者提交的 bio 会展示给其他用户。
步骤 2
字符串拼接构造响应
控制器把值拼进 HTML 或 <script> 块。使用无害的测试值 <img src=x onerror=alert(1)> 时,返回的字符串中包含一个真实的标签;使用 ';alert(1);// 时,脚本块多出一条新语句。
步骤 3
浏览器解析它
由于方法声明了 text/html,浏览器把响应体当作页面渲染。图片加载失败并触发 onerror,或者脚本块执行被注入的语句。
步骤 4
脚本以网站的身份运行
代码以应用的源(origin)执行:它可以读取 DOM,带着受害者的会话 cookie 调用应用的接口,并篡改用户看到的内容。
源代码对比:漏洞与安全实现
存在漏洞的实现
// ProfileController.java:每个接口都把请求数据拼进 HTML
@RestController
public class ProfileController {
// HTML 上下文:查询值被当作标记插入
@GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
return "<h1>Results for " + q + "</h1>";
}
// JavaScript 上下文:user 中的引号会结束字符串字面量
@GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
public String widget(@RequestParam String user) {
return "<script>const user = '" + user + "';</script>";
}
// 富文本连同所有标签和事件处理器原样返回
@PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
public String preview(@RequestParam String bio) {
return "<div class=\"bio\">" + bio + "</div>";
}
}
加固后的安全修复
// ProfileController.java:每个值都按其上下文编码或经过净化
@RestController
public class ProfileController {
private static final PolicyFactory BIO_POLICY = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
// forHtml 为元素内容编码 < > & " '
@GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
return "<h1>Results for " + Encode.forHtml(q) + "</h1>";
}
// forJavaScript 转义引号、反斜杠和 </script> 序列
@GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
public String widget(@RequestParam String user) {
return "<script>const user = '" + Encode.forJavaScript(user) + "';</script>";
}
// 净化器保留格式和链接,其余一律丢弃
@PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
public String preview(@RequestParam String bio) {
return "<div class=\"bio\">" + BIO_POLICY.sanitize(bio) + "</div>";
}
}
工程与系统安全加固清单
- ✓在 SpotBugs 中启用 Find Security Bugs,遇到
XSS_SERVLET、XSS_JSP_PRINT和XSS_REQUEST_PARAMETER_TO_SERVLET_WRITER时让构建失败。 - ✓在 CI 中搜索模板里的
th:utext、[(、<%=以及 JSP 中裸露的${...};每一处都必须输出经过净化或常量的内容。 - ✓只要控制器自己拼装标记,就用 OWASP Java Encoder 按确切的上下文编码(
forHtml、forHtmlAttribute、forJavaScript、forUriComponent)。 - ✓用户提供的 HTML 在进入
th:utext或响应体之前,先经过 OWASP Java HTML Sanitizer 的策略。 - ✓为 Spring Security 的
headers().contentSecurityPolicy(...)配置基于 nonce 的script-src,并让会话 cookie 保持HttpOnly和SameSite=Lax。