xss防护核心是按上下文做输出编码,而非输入过滤或删标签;innerhtml赋值前必须转义,否则直接插入用户数据等于开后门。

直接结论:XSS 防护不是“加一层过滤”就能解决的事,核心动作是「按上下文做输出编码」,而不是靠输入过滤或删标签。
innerHTML 赋值前必须转义,否则等于开后门
常见错误现象:element.innerHTML = userInput 直接插入用户数据,哪怕只含一个 或 <code>",就可能触发 <img src="x" onerror="alert(1)"> 这类 payload。
使用场景:前端 JS 动态渲染评论、搜索结果、表单回显等。
实操建议:
- 优先改用
element.textContent = userInput—— 纯文本显示,零风险,浏览器完全不解析 HTML - 若必须渲染 HTML(如富文本),先用
DOMPurify.sanitize(dirtyHtml)处理,再赋给innerHTML;别自己写正则删<script></script> - 服务端返回的 JSON 字段若已做过 HTML 转义(如
"<div>hello</div>"),前端不能再二次转义,否则会显示为字面量
模板引擎里变量插值要确认是否默认转义
常见错误现象:EJS、Jinja2、Thymeleaf 等模板中写 {{ userInput }} 却没开启自动转义,导致用户输入的 <script></script> 直接进 DOM。
使用场景:服务端渲染页面(SSR)、邮件模板、后台管理界面。
实操建议:
- PHP 用
htmlspecialchars($str, ENT_QUOTES, 'UTF-8');Java JSTL 用<out value="${userInput}"></out> - Jinja2 默认转义,但若用了
{{ userInput | safe }}就绕过了——确认每个| safe都有对应净化逻辑 - Node.js 的 EJS 默认不转义,必须手动包裹
或改用
把用户数据塞进 HTML 属性时,引号和事件属性要分开防
常见错误现象:<div title="{{ userInput }}"> 中输入 <code>" onclick="alert(1),最终变成 <div title="" onclick="alert(1)">。<p>使用场景:动态生成 <code>data-id、title、alt、href 等属性值。
实操建议:
- 双引号包围的属性(
title="xxx")必须转义"→";单引号包围(title='xxx')则转义'→' - URL 类属性(
href、src)不能只 HTML 转义,得先encodeURIComponent()再拼入,否则javascript:alert(1)仍可执行 - 绝对避免
<button onclick="doSomething('{{ userInput }}')"></button>这种写法——事件处理器内嵌用户数据,无解
CSP 是兜底策略,但不能替代输出编码
常见错误现象:加了 Content-Security-Policy: script-src 'self' 就以为 XSS 没事了,结果攻击者用 <img src="x" onerror="eval(atob('YWxlcnQoMSk='))"> 绕过。
使用场景:所有生产环境 HTML 响应头、Nginx/Apache 配置、或 <meta http-equiv="Content-Security-Policy">。
实操建议:
- 必须配
default-src 'self'+script-src 'self' https://cdn.example.com,禁掉unsafe-inline和unsafe-eval - 对富文本场景,可加
frame-src 'none'和object-src 'none'禁用<iframe></iframe>、<object></object> - CSP 报告模式(
report-uri或report-to)要开,用于发现漏掉的 XSS 注入点
最容易被忽略的一点:同一个字符串,在不同上下文(HTML 文本、属性、JS 字符串、URL)里需要不同的编码方式。没有“一劳永逸”的转义函数,也没有“全局过滤中间件”能真正解决问题。你得清楚每一处 userInput 最终落进哪一层解析器,再选对应的处理逻辑。











