防范XSS的核心是永远不信任用户输入,始终对输出上下文做转义;禁用innerHTML等危险操作,优先使用textContent或createElement;服务端模板需启用默认转义,慎用safe标记;CSP仅为辅助防线,不可替代编码层防护。

HTML 代码注入(即 XSS,跨站脚本攻击)不是“HTML 本身的功能”,而是因开发者把用户输入未经处理直接拼入 HTML 输出,导致浏览器执行了意外的脚本。防范核心只有一条:永远不信任用户输入,始终对输出上下文做转义。
为什么 innerHTML + 用户输入 = 高危操作
直接将用户提交的内容赋给 innerHTML,等于告诉浏览器:“这段字符串就是 HTML,请解析并执行”。哪怕只是 这样的字符串,也会触发执行。
常见错误场景:
- 评论区渲染用户输入时用了
el.innerHTML = userInput - 搜索结果页把关键词插进 HTML:
document.write(`您搜索了:${keyword}`) - 富文本编辑器未限制标签,又用
innerHTML渲染输出
这些做法在现代前端开发中应被彻底禁止。
立即学习“前端免费学习笔记(深入)”;
安全替代方案:textContent 和 createElement
textContent 是最简单、最安全的文本插入方式——它把内容当纯文本,自动转义所有 HTML 特殊字符( → zuojiankuohaophpcn," → " 等)。
const userInput = '';
const el = document.getElementById('output');
el.textContent = userInput; // 页面显示的就是字面量,不会执行
如需动态构建结构化内容(比如带 class 的 ),应使用 createElement + appendChild,而非字符串拼接:
const userInput = 'Hello & World';
const span = document.createElement('span');
span.className = 'highlight';
span.textContent = userInput; // 安全
el.appendChild(span);
注意:innerText 不推荐用于安全目的,它受 CSS 样式影响(如 display: none 的内容不计入),且在不同浏览器中行为不一致。
服务端模板也要防:Jinja2、EJS、Django 等默认转义机制别绕过
即使不用前端 JS 拼接,服务端模板若关闭自动转义,同样危险。例如:
- EJS 中
是不转义的,应改用(默认转义) - Jinja2 中
{{ userInput }}默认转义,但{{ userInput | safe }}会绕过——除非你 100% 确认该变量来自可信源且已过滤 - Django 模板同理:
{{ user_input }}安全,{{ user_input|safe }}危险
关键判断点:只要变量来源含用户输入(表单、URL 参数、数据库读取),就必须走转义路径;手动标记 safe 是最后手段,且必须配套白名单过滤逻辑。
Content Security Policy(CSP)是最后一道防线,但不能替代编码层防护
CSP 可以阻止内联脚本和未授权域名的资源加载,例如通过响应头设置:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com
它能缓解 XSS 后果(比如阻止 onerror 执行或外连窃取),但无法阻止 DOM XSS 的初始触发。例如:
- 用户输入被写入
location.href或eval(),CSP 通常不拦截 - 利用已授权 CDN 域名托管恶意脚本(白名单被滥用)
- 纯前端路由参数被直接
innerHTML插入,CSP 对此无能为力
所以 CSP 是纵深防御的一环,不是“开了就万事大吉”的开关。真正可靠的安全,始于对每个输出点的上下文感知和精准转义。











