XSS 测试与修复:按输出上下文逐层拆解¶
XSS 修不好,通常不是“过滤不够狠”,而是编码用错了上下文。同一个 < 在 HTML 文本、属性、JS 字符串里需要不同处理。
1. 先判断输出位置¶
输入 xss123,看它出现在响应哪里:
<!-- A. HTML 文本 -->
<div>hello xss123</div>
<!-- B. 属性值 -->
<input value="xss123">
<!-- C. 事件/JS 字符串 -->
<script>var name = 'xss123';</script>
<!-- D. URL -->
<a href="/search?q=xss123">...</a>
四种位置的 payload 完全不同。用 <script>alert(1)</script> 通测,会漏掉 B/C/D 三类。
2. 各上下文的可用 payload¶
A. HTML 文本¶
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<details open ontoggle=alert(1)>
B. 属性值内(双引号包裹)¶
单引号包裹时换成 ' onmouseover='alert(1)。
C. JS 字符串内¶
如果字符串进入 eval 或 setTimeout,甚至不需要闭合。
D. URL / href¶
浏览器对 javascript: 的解析会忽略空白和控制字符:
3. DOM 型 XSS:只看源码会漏¶
危险 source → sink 组合:
// source
location.hash
location.search
document.referrer
window.name
// sink
element.innerHTML = userInput
document.write(userInput)
eval(userInput)
new Function(userInput)
setTimeout("doSomething(" + userInput + ")", 0)
element.setAttribute("href", userInput)
验证步骤:
- 在 DevTools 里给 source 打断点(如
location.hash) - 传
#<img src=x onerror=alert(1)> - 观察是否流入
innerHTML - 如果被
encodeURIComponent处理过,再试二次解码路径
用 Playwright 自动化检测弹窗:
from playwright.sync_api import sync_playwright
payloads = [
"<img src=x onerror=alert(1)>",
"\" onmouseover=\"alert(1)",
"javascript:alert(1)",
]
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
dialogs = []
page.on("dialog", lambda d: (dialogs.append(d.message), d.dismiss()))
for pl in payloads:
page.goto(f"https://target/search?q={pl}")
page.wait_for_timeout(800)
print("executed:", dialogs)
browser.close()
4. 存储型 XSS 的价值点¶
这些字段最容易被忽略:
| 位置 | 为什么危险 |
|---|---|
| 昵称、签名 | 在个人主页、评论区、排行榜多处渲染 |
| 收货地址 | 后台订单详情渲染 |
| 工单/客服消息 | 客服后台、导出 Excel |
| 上传文件名 | 文件列表直接显示,可能未编码 |
| SVG 上传 | SVG 内可执行脚本 |
| 富文本 | 白名单不严时直接落库 |
存储型要验证跨角色触发:用户提交,管理员后台打开时执行——这类漏洞危害远高于反射型。
5. 修复:编码 + 白名单 + CSP¶
5.1 按上下文编码(Java)¶
import org.owasp.encoder.Encode;
// HTML 文本
String safe = Encode.forHtml(userInput);
// HTML 属性
String attr = Encode.forHtmlAttribute(userInput);
// JS 字符串
String js = Encode.forJavaScript(userInput);
// URL 参数
String url = Encode.forUriComponent(userInput);
Spring 项目里,Thymeleaf 默认对 th:text 转义;危险的是 th:utext(不转义)。JSP 里用 <c:out> 而不是 <%= %>。
5.2 富文本白名单(DOMPurify)¶
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ["p", "br", "strong", "em", "ul", "ol", "li", "a", "img"],
ALLOWED_ATTR: ["href", "src", "alt", "title"],
ALLOW_DATA_ATTR: false,
FORBID_TAGS: ["style", "iframe", "form"],
FORBID_ATTR: ["onerror", "onload", "onclick"],
});
document.getElementById("content").innerHTML = clean;
服务端也要做同样的白名单(如 OWASP Java HTML Sanitizer),不能只依赖前端。
5.3 CSP:兜底而不是唯一手段¶
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-{RANDOM}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
frame-ancestors 'self';
" always;
注意:
script-src 'unsafe-inline'会让 CSP 对 XSS 基本失效nonce必须每次响应随机,不能复用- 先以
Content-Security-Policy-Report-Only上线观察,避免直接打断业务
5.4 Cookie 兜底¶
HttpOnly 让 XSS 拿不到会话 Cookie,但不能阻止 XSS 以当前用户身份发请求,所以只是降低危害。
6. 绕过与修复的对抗验证¶
修复后不要只测原始 payload:
□ <ScRiPt>alert(1)</ScRiPt>
□ <scr<script>ipt>alert(1)</script>
□ <img src=x onerror=alert(1)>
□ " onmouseover="alert(1)
□ ');alert(1);//
□ javascript:alert(1)(href)
□ %3Cscript%3Ealert(1)%3C/script%3E(二次解码)
□ <svg><animate onbegin=alert(1)>
□ 富文本:<a href="javascript:alert(1)">x</a>
□ Markdown 渲染:)
如果只过滤了 <script>,第二、三、四条都会过。
7. 一个修复失败的典型¶
现象:用户昵称已做 replace("<", "<"),但个人主页仍弹窗。
原因:昵称被渲染进 onclick="showUser('昵称')",单引号未处理,payload 为 ');alert(1);//。
修复:该位置改用 Encode.forJavaScript,并把昵称通过 data-* 属性传递,事件里读属性而不是拼接字符串。