【安全研发】XSS (跨站脚本攻击) 深度复盘与实战笔记
记录时间: 2026-01-20
核心逻辑: 永远不要相信用户的输入。 必须严格区分 数据 (Data) 与 代码 (Code)。
1. 核心原理:什么是 XSS?
Cross Site Scripting。本质是黑客把恶意的 JavaScript 代码注入到了我的网页里,当受害者访问时,浏览器误以为那是正常的代码去执行了。
攻击后果:
- 偷取 Cookie / SessionID(导致账号被盗)。
- 监听用户键盘输入。
- 伪造用户操作(如自动转账)。
2. 三大攻击类型(记忆口诀)
| 类型 | 存储位置 | 危险等级 | 攻击原理速记 |
|---|---|---|---|
| 存储型 (Stored) | 数据库 | ⭐⭐⭐⭐⭐ (最高) | 恶意代码存入 DB。一人发帖,万人中招。持久化攻击。 |
| 反射型 (Reflected) | URL参数 | ⭐⭐⭐ | 恶意代码在链接里。需要诱导用户点击链接,后端把参数"反射"回页面。 |
| DOM 型 (DOM-based) | HTML DOM | ⭐⭐⭐ | 纯前端漏洞。后端不背锅。JS 代码乱改页面结构导致执行脚本。 |
3. 实战:DOM 型 XSS 的"帮凶"们
这是今天复习的重点,涉及两个关键 API:document.write 和 URLSearchParams。
3.1 漏洞代码演示 (document.write)
document.write 是上古时代的 API,它有两个致命问题:
- 逻辑炸弹: 如果页面加载完后调用,会清空整个页面。
- 安全黑洞: 直接解析 HTML 字符串,如果包含
<script>会被执行。
攻击场景复现:
javascript
// 1. 现代化的获取 URL 参数方式 (记忆点:URLSearchParams)
// 假设 URL 是:http://localhost/index.html?name=<script>alert(document.cookie)</script>
const params = new URLSearchParams(window.location.search);
const userName = params.get("name"); // 拿到了恶意脚本字符串
// 2. 致命错误:直接写入 DOM
// 浏览器会把 userName 当作 HTML 解析,发现里面有 script 标签,于是执行!
if (userName) {
document.write("欢迎你:" + userName);
}
3.2 为什么 URLSearchParams 这么顺手?
它是 ES6 后的原生 API,替代了以前复杂的正则匹配。
params.get("key"): 获取值。params.has("key"): 判断存在。- 正因为它获取参数太容易,如果开发者拿到参数后不做 HTML 转义 就渲染,XSS 就发生了。
4. 防御体系:如何把"代码"变成"文本"?
4.1 核心防御:HTML 转义 (Escaping)
让浏览器**"只读其文,不从其令"**。
- 攻击者输入:
<script>alert(1)</script> - 后端/前端转义后:
<script>alert(1)</script> - 浏览器渲染结果: 屏幕上显示出
<script>...这行字,但不执行它。
4.2 其他防御层
- HttpOnly Cookie: 给敏感 Cookie 加上
HttpOnly属性,让 JS 读不到(document.cookie读出来是空的),防止 Token 被偷。 - CSP (内容安全策略): 在 HTTP 头里限制浏览器只能加载特定域名的 JS 文件,禁止内联脚本。
5. 灵魂 Q&A (今日复盘总结)
Q1: XSS 是不是只要把 < 和 > 转义了就安全了?
A: 90% 的情况是。但在某些特殊属性(如
href或onclick)里,还需要注意引号的转义,防止属性逃逸。
Q2: 既然 document.write 这么危险,为什么还有人用?
A: 主要是老旧代码或第三方广告脚本在用。现代开发(Vue/React)几乎不用它,而是用虚拟 DOM。但如果用了
v-html或dangerouslySetInnerHTML,依然会有 XSS 风险。
Q3: 遇到存入数据库的 XSS(存储型),是在"入库时"转义,还是"出库显示时"转义?
A: 最佳实践是"出库显示时"转义。
- 入库保留原始数据(Raw Data),保证数据的纯洁性(万一以后要用于非 HTML 场景呢?)。
- 谁显示,谁负责转义(Context-Aware Encoding)。
(End of Notes - 2026.01.20 晚)