C04 XSS 与 CSP
摘要:页面把用户输入原样插进 HTML 时,输入里的标签会被浏览器当成结构执行,这就是跨站脚本。风险取决于插入位置:元素内容、属性、URL 与脚本块四种上下文的转义规则互不通用。本文给出四种上下文的正确写法、一份可用的 CSP 策略,以及三段注入演练,验证防护是否生效。
关键词:XSS,内容注入,转义,CSP,子资源完整性
所属:frontend/html / C-高级 | 上一篇:C03-资源提示与性能 | 下一篇:C05-HTML与工程化
1 引言
注入不是脚本层的问题:浏览器解析 HTML 时不区分「这段文字来自数据库」与「这段文字来自模板」。只要内容被拼进 HTML 文本,标签就会生效。
本文覆盖四种上下文的处理方式与一条纵深防护线。范围见表 1-1。
| 项 | 本文覆盖 | 本文不覆盖 |
|---|---|---|
| 注入 | 元素内容、属性、URL、脚本块四种上下文 | 服务端框架的自动转义实现 |
| 防护 | 转义、白名单净化、CSP、SRI、rel="noopener" |
CSRF、SQL 注入等其它风险 |
| 验证 | 三段注入演练与响应头检查 | 渗透测试与漏洞扫描工具 |
2 预备知识
| 项 | 要求 | 核对方式 |
|---|---|---|
| 环境 | A02 的本地服务器 | 能访问 localhost |
| 前置 | A07 | 能写表单与提交处理 |
| 前置 | B03 | 理解 sandbox 与同源策略 |
| 文件 | html-practice/xss.html、render.mjs |
一个页面 + 一个渲染脚本 |
3 原理与实现
3.1 四种上下文
同一段恶意字符串,落在不同位置时需要的防护完全不同,如图 3-1 所示。
flowchart TD
U["用户输入"] --> C{"插入位置"}
C -->|"元素内容"| C1["HTML 实体转义<br/>< > &"]
C -->|"属性值"| C2["引号包裹 + 实体转义"]
C -->|"URL"| C3["协议白名单 + 编码"]
C -->|"脚本块"| C4["禁止拼接<br/>改用 JSON 序列化"]
图 3-1 按上下文选择防护手段
图上可见:一次转义不能覆盖全部位置;只做 HTML 实体转义时,
href="javascript:..."仍然可以执行。
3.2 危险写法与安全写法
// 运行环境:Node 20+;保存为 render.mjs,执行 node render.mjs
import { writeFileSync } from 'node:fs';
const userInput = process.argv[2] ?? '<img src=x onerror="alert(1)">';
// 不安全:直接拼接
const unsafe = `<p>${userInput}</p>`;
// 安全:HTML 实体转义
const escapeHtml = (s) =>
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
// 安全:URL 只允许 http/https/相对路径
const safeUrl = (u) => (/^(https?:|\.{0,2}\/)/i.test(u) ? escapeHtml(u) : '#');
const page = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src 'self' https: data:; script-src 'self'" />
<title>注入演练</title>
</head>
<body>
<h1>输出结果</h1>
<p>${escapeHtml(userInput)}</p>
<p><a href="${safeUrl(userInput)}">链接</a></p>
<!-- 不安全示例,仅供对照:<p>${userInput}</p> -->
<script src="./app.js"></script>
</body>
</html>`;
writeFileSync('xss.html', page, 'utf8');
console.log('已生成 xss.html');
四种上下文的处理规则见表 3-1。
| 上下文 | 危险示例 | 处理方式 |
|---|---|---|
| 元素内容 | <p>${input}</p> |
实体转义 < > & " |
| 属性值 | <div title=${input}> |
引号包裹 + 实体转义 |
| URL | <a href="${input}"> |
协议白名单,拦截 javascript: 与 data: |
| 脚本块 | var name = '${input}' |
禁止拼接,改用 JSON 序列化 |
| 富文本 | 直接渲染用户 HTML | 白名单净化库,只保留允许的标签与属性 |
CSP 常用指令见表 3-2。
| 指令 | 作用 | 建议值 |
|---|---|---|
default-src |
默认资源来源 | 'self' |
script-src |
脚本来源 | 'self',避免 'unsafe-inline' |
style-src |
样式来源 | 'self',必要时用哈希替代内联 |
img-src |
图片来源 | 'self' https: data: |
frame-ancestors |
允许被谁内嵌(防点击劫持) | 'none' 或指定域名 |
upgrade-insecure-requests |
升级 http 子资源 | 生产环境开启 |
外部资源的完整性校验与链接安全见表 3-3。
| 场景 | 写法 |
|---|---|
| 引入第三方脚本 | <script src="..." integrity="sha384-..." crossorigin="anonymous"> |
| 新窗口打开链接 | rel="noopener noreferrer" |
| 内嵌第三方页面 | 空 sandbox 起步,按需放行(B03) |
| 上传文件回显 | 只回显服务端生成的地址,不回显原始文件名 |
4 验证与讨论
- 执行
node render.mjs '<img src=x onerror="alert(1)">',打开生成的xss.html,确认页面显示的是文本而非执行脚本。 - 把模板中被注释的不安全示例取消注释,重新生成并打开,确认弹出警告框,理解差异。
- 执行
node render.mjs 'javascript:alert(1)',点击生成的链接,确认被替换为#。 - 打开开发者工具 Network 面板查看首页响应头,确认
Content-Security-Policy已生效;在 Console 中执行document.body.innerHTML = '<script>alert(1)<\/script>',观察 CSP 是否阻断。
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| 内容注入 | 显示为文本,无弹窗 | 实体转义 |
| 属性注入 | 属性值被转义,未新增属性 | 引号 + 转义 |
| URL 注入 | javascript: 被替换为 # |
协议白名单 |
| CSP 生效 | Console 显示违规被拒绝 | script-src 'self' |
| 第三方脚本 | 篡改内容后校验失败、脚本不执行 | SRI |
讨论与局限。 meta 形式的 CSP 无法使用 frame-ancestors 等部分指令,生产环境应由响应头下发。另 CSP 不能防住服务端模板拼接错误,转义仍是第一道防线;两者是纵深层级,不是替代关系。
5 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 只转义了尖括号仍被注入 | 属性未加引号,空格分隔出新的属性 | 属性值一律用双引号包裹 |
| 富文本净化后样式全丢 | 白名单过严,把 class 也过滤了 |
按需放行允许的属性 |
| CSP 上线后页面白屏 | 内联脚本被 script-src 'self' 阻断 |
改为外链脚本,或用哈希/nonce |
| 第三方脚本加载失败 | SRI 哈希与文件不一致(对方已更新) | 固定版本地址,更新时同步哈希 |
| 新窗口打开的页面能操作原页 | 缺少 noopener |
补 rel="noopener noreferrer" |
6 结论
| 序号 | 结论 | 依据 |
|---|---|---|
| 1 | 注入风险由插入位置决定,转义规则随之不同 | 图 3-1、表 3-1 |
| 2 | URL 与富文本必须走白名单,不做黑名单过滤 | 表 3-1 |
| 3 | CSP 防内联脚本,转义防拼接,二者叠加 | 表 3-2 |
| 4 | 第三方资源用 SRI 锁定内容 | 表 3-3 |
- 上一篇:C03-资源提示与性能
- 下一篇:C05-HTML与工程化
- 返回:C00-总结 | 科目 README
参考文献
[1] OWASP · Cross Site Scripting Prevention Cheat Sheet,https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html(访问于 2026-10-29)
[2] MDN Web Docs · 内容安全策略 CSP,https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP(访问于 2026-10-29)

评论 0
还没有评论,快来抢沙发吧~