模板引擎默认转义不可靠。不同引擎行为差异大,EJS默认转义但存在例外,Vue中{{}}转义而v-html不转义,Twig需显式使用|raw过滤器;服务端必须提前对所有用户输入进行HTML实体编码,前端应优先用textContent,必需innerHTML时须用DOM构造法或DOMPurify白名单过滤。

模板引擎默认转义是否可靠
不可靠。不同引擎行为差异极大,且常被开发者误信“开了自动转义就万事大吉”。EJS 默认转义 ,但 完全跳过;Vue 的 {{ data }} 转义,v-html 却直接执行;Twig 中 {{ data|raw }} 等同于裸奔。上线前必须逐个确认每个变量插值所用的语法路径是否实际走转义逻辑。
服务端模板中用户输入字段必须提前转义
所有动态内容——表单提交值、URL 参数、数据库读取字段——在插入模板前就必须完成 HTML 实体编码。这不是可选项,是底线。漏掉任意一处,比如 title="{{ user_input }}" 里没处理单引号,攻击者输 " onclick="alert(1) 就会生成可执行的属性。
- PHP:用
htmlspecialchars($input, ENT_QUOTES, 'UTF-8'),缺第三个参数可能被 GBK 截断绕过 - Python:用
html.escape(input, quote=True),quote=True才确保单双引号都被处理 - Java(JSTL):用
<c:out value="${userInput}" escapeXml="true"/>,别依赖 EL 表达式默认行为 - Node.js:用
he.escape(input),避免entities库——它默认不转义单引号
前端 JS 拼接 HTML 字符串时的致命陷阱
常见错误是把后端已转义的数据再拼进字符串,比如后端返回 {"name": "<script>hello</script>"},前端却写 el.innerHTML = `<div>${data.name}</div>`。结果浏览器解析的是字面量 <script>,而非执行脚本——看似安全,实则掩盖了问题:一旦某处漏转义或二次拼接,立刻崩盘。
- 纯文本展示一律用
textContent,零成本、零风险,完全绕过 HTML 解析器 - 必须用
innerHTML时,禁止直接赋值原始字符串;优先走 DOM 构造法:function htmlEscape(str) { const el = document.createElement('div'); el.textContent = str; return el.innerHTML; } - 富文本场景下,先用
DOMPurify.sanitize(dirtyHtml)白名单过滤,再赋给innerHTML;禁用黑名单模式(如FORBID_TAGS: ['script']),只显式声明ALLOWED_TAGS
属性值、URL、JS 上下文混用导致的转义失效
把用户数据塞进 href、onclick 或内联 script 时,HTML 实体转义完全不够用。例如 <a href="https://www.php.cn/link/53763bcddb83f147196d1d54a91ccc24">link</a>,若 user_url 是 javascript:alert(1),转义 和 <code>& 无济于事。
立即学习“前端免费学习笔记(深入)”;
-
href/src类属性:先用encodeURIComponent()编码 URL,再拼入带引号的属性值 -
onclick等事件属性:绝对禁止拼接用户输入;改用addEventListener+ 数据绑定 - JSON 插入 inline script:用
JSON.stringify(),不是简单替换;否则引号和反斜杠会破坏 JS 语法 - 模板中混用上下文:Jinja2 的
{{ value|e }}是 HTML 转义,但放进href="{{ url }}"时,url必须已是合法、协议白名单校验过的绝对路径
真正容易被忽略的是上下文切换点:同一段用户输入,可能既出现在 <p></p> 文本中,又作为 data-id 属性值,还参与生成某个 fetch 请求的 URL。每种位置对应不同的转义规则,不能复用一个函数打天下。



















