一次 XSS 的完整复现与防御
一次 XSS 的完整复现与防御
站长XSS(跨站脚本)是出现频率最高的一类前端漏洞。很多人对它的印象停留在「弹个窗」,但它真正危险的地方在于:脚本能做任何用户能做的事。
三种类型
| 类型 | 触发方式 | 典型场景 |
|---|---|---|
| 反射型 | 恶意代码在 URL 里,服务端原样返回 | 搜索结果页、错误提示页 |
| 存储型 | 恶意代码被存进数据库 | 评论区、昵称、个性签名 |
| DOM 型 | 前端 JS 把不可信数据写进 DOM | innerHTML = location.hash |
复现:从 alert 到盗号
第一步:确认有没有洞
在搜索框输入:
1 | <script>alert(1)</script> |
如果页面弹窗了,说明后端把输入原样输出了。
第二步:换成真正有害的载荷
1 | <script> |
只要受害者访问了这个页面,Cookie 就到攻击者手上了。如果 Cookie 里带着会话 ID,且没有 HttpOnly,账号就丢了。
第三步:绕过常见过滤
很多站点会过滤 <script>,但可以绕:
1 | <img src=x onerror=alert(1)> |
所以黑名单过滤基本没用,必须做输出编码。
防御:四道防线
1. 输出编码(最重要)
根据输出位置选对应的编码方式:
| 输出位置 | 该做的编码 |
|---|---|
| HTML 文本节点 | HTML 实体编码(< → <) |
| HTML 属性 | 属性编码,并且属性值必须加引号 |
| JS 字符串里 | JS 转义(\ ' ") |
| URL 参数 | encodeURIComponent |
现代框架已经默认帮你做了:
- React:
{}插值默认转义,只有dangerouslySetInnerHTML会跳过 - Vue:
{{ }}默认转义,只有v-html会跳过
原则:永远不要用 v-html / dangerouslySetInnerHTML 渲染用户输入。
2. CSP(内容安全策略)
加一个响应头,从根上限制脚本来源:
1 | add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'"; |
配了 CSP 之后,即使有漏网的 XSS,内联脚本也执行不了。这是性价比最高的一道防线。
3. Cookie 加 HttpOnly
1 | Set-Cookie: sessionid=xxx; HttpOnly; Secure; SameSite=Lax |
HttpOnly 之后 JS 读不到 Cookie,就算 XSS 成功也偷不走会话。
4. 富文本要专门处理
如果业务确实需要用户发 HTML(比如博客评论支持加粗),必须走白名单消毒,用 DOMPurify 这类成熟库,不要自己写正则。
1 | npm install dompurify |
1 | import DOMPurify from 'dompurify'; |
自查清单
- 所有用户输入输出时都做了编码
- 没有任何地方用
innerHTML/v-html渲染用户输入 - 配了 CSP
- 会话 Cookie 带
HttpOnly - 富文本走白名单消毒
记住一句话:转义是防守,CSP 是保险,HttpOnly 是底裤。三层都穿上才敢睡觉。