必须在服务端模板渲染前对所有用户输入字段(表单值、url参数、数据库读取内容)进行html实体编码,否则未转义的、&、"、'等字符可能触发xss或dom结构破坏。

服务端模板渲染前必须做 HTML 实体编码
所有用户输入字段——表单值、URL 参数、数据库读取内容——在塞进 EJS / Nunjucks / Twig 模板前,必须完成 html.escape 或等效处理。漏掉任意一处,比如 title="{{ user_input }}" 里没转义单引号,攻击者输 " onclick="alert(1) 就会生成可执行属性。
不同语言的正确写法:
- Python:
html.escape(input, quote=True)——quote=True才确保单双引号都被转成"和' - Node.js:
he.escape(input)—— 别用entities库,它默认不转义单引号 - PHP:
htmlspecialchars($input, ENT_QUOTES, 'UTF-8')—— 缺第三个参数可能被 GBK 截断绕过 - Java(JSTL):
<out value="${userInput}" escapexml="true"></out>—— 别依赖 EL 表达式默认行为
EJS/Nunjucks 中 和 的语义陷阱
很多人以为开了自动转义就安全了,其实 EJS 的 默认转义,但 完全不转义;Nunjucks 同理,{{ }} 转义,{% raw %}...{% endraw %} 或 |safe 过滤器等于裸奔。上线前必须逐个确认每个插值点用的是哪个语法。
常见踩坑场景:
- 富文本字段用
→ 被 XSS,应改用(服务端调用) - 拼接内联 JS:
<script>const name = '<%= user.name %>'</script>→ 单引号逃逸,必须先 JSON.stringify 再转义 - 动态 class 名:
<div class="btn <%= status === 'active' ? 'active' : '' %>"> → status 若含空格或引号,直接破坏 HTML 结构<h3>拦截逻辑不能只靠模板引擎默认行为</h3> <p>模板引擎的“自动转义”不是银弹。Vue 的 <code>{{ }}转义,但v-html不转;Twig 的{{ data|raw }}绕过一切;EJS 的也不走转义。更危险的是:某些引擎对属性值插值(如href="{{ url }}")不校验协议,用户填javascript:alert(1),转义后仍是可执行的javascript:alert(1)。所以真实拦截必须分层:
- 服务端:对所有用户输入字段,在进入模板前做
html.escape+ 协议白名单校验(如只允许https?、/、#) - 响应头:加
Content-Security-Policy: script-src 'self'; object-src 'none',堵死 inline script 和 eval - 前端:禁止用
innerHTML = `...${data}...`拼接,优先用textContent;非得用 innerHTML 时,先过DOMPurify.sanitize()(服务端版)
自动化检查必须覆盖 SSR 输出片段的结构完整性
用
renderToString或renderTemplate生成 HTML 字符串后,不能直接res.send(htmlString)。服务端吐出的内容必须是纯 fragment,不含多余或标签,否则客户端 hydration 时 DOM 结构不匹配,导致静默失败:光标丢失、焦点错乱、状态不同步。检查项要包括:
- 输出字符串是否以
<div id="root"> 开头、以 <code>
- 服务端:对所有用户输入字段,在进入模板前做











