SSR模板中变量插值必须开启自动转义,否则用户输入直接插入HTML将触发innerHTML级XSS漏洞;EJS默认不转义,Jinja2默认转义但|safe可绕过,PHP需用htmlspecialchars($str, ENT_QUOTES, 'UTF-8'),Next.js的dangerouslySetInnerHTML必须配合DOMPurify使用。

SSR模板中变量插值必须开启自动转义
服务端渲染时,用户输入若未经处理直接插入HTML,innerHTML级漏洞会立刻暴露——哪怕只渲染一个昵称或搜索关键词。EJS、Jinja2、Thymeleaf等模板引擎默认行为差异极大,不能靠“我以为它转义了”来赌。
- EJS 默认不转义:
直接输出原始内容,必须改用或手动包裹escapeHTML() - Jinja2 默认转义,但
{{ userInput | safe }}会绕过,每个| safe都得对应 DOMPurify 或严格白名单校验 - PHP 的
htmlspecialchars($str, ENT_QUOTES, 'UTF-8')是底线,别漏掉第三个参数,否则 UTF-8 双字节字符可能被截断导致编码绕过 - Next.js 的
dangerouslySetInnerHTML是明确警告:它不转义、不净化、不兜底,除非你已用DOMPurify.sanitize()处理过,否则等于主动开后门
动态生成HTML片段时禁止拼接字符串
前端 JS 拼接 HTML 字符串(如 el.innerHTML = '<div>' + userInput + '</div>')是 XSS 高危操作,浏览器解析时不会做任何过滤,攻击者输入 " 或 <img onerror="alert(1)" alt="动态SSR架构中防范跨站脚本注入与提升HTML代码质量方案" > 就直接执行。
- 纯文本场景一律用
textContent—— 它不解析标签,也不触发事件,零风险 - 富文本必须走净化流程:
DOMPurify.sanitize(dirtyHtml, { ALLOWED_TAGS: ['p', 'br', 'img'], ALLOWED_ATTR: ['src', 'alt'] }),禁用on*和javascript:协议 - 服务端返回的 HTML 片段,前端不能无脑
innerHTML赋值;即使后端声称“已过滤”,也要在客户端再过一遍 DOMPurify —— SSR 和 CSR 渲染链路不同,信任不能跨环节传递 - URL 类属性(
href、src)不能只 HTML 转义,必须先encodeURIComponent()再拼入,否则javascript:alert(1)仍可执行
nonce 必须由服务端动态生成并两端对齐
SSR 场景下想保留少量内联脚本(如初始化状态),nonce 是唯一安全出口,但硬编码、复用、大小写不一致、模板插值失败都会让整个机制失效。
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
- 服务端每次响应生成 32 字节以上十六进制随机值:
crypto.randomUUID()(Node.js)或random_bytes(32)(PHP),绝不能从 URL、localStorage 或固定字符串读取 - HTTP 响应头必须完整写入:
Content-Security-Policy: script-src 'self' 'nonce-27f434278e6b79578d5c9f0a321c4567',大小写敏感,缺一不可 - HTML 中对应
<script></script>标签必须显式携带相同值:<script nonce="27f434278e6b79578d5c9f0a321c4567"></script>,且该脚本必须是首屏首个执行的 JS,否则'strict-dynamic'不生效 - Webpack/Vite 的 runtime 脚本(HMR、chunk 加载)默认不带 nonce,需通过插件(如
webpack-csp-plugin)注入,否则会被 CSP 拦截
CSP 指令不能退化为 'unsafe-inline'
加了 Content-Security-Policy 就以为万事大吉?错。很多项目最终 fallback 到 script-src 'unsafe-inline',等于把门钥匙交给攻击者。
立即学习“前端免费学习笔记(深入)”;
-
script-src 'self'能拦住<script>alert(1)</script>、onclick=、javascript:,但它不防 DOM 型 XSS(如location.search直接塞innerHTML) - 若必须支持内联,只用
'nonce-xxx',禁用'unsafe-inline';若要用'strict-dynamic',必须和'nonce-xxx'共存,且首个脚本带该 nonce -
<meta http-equiv="Content-Security-Policy">完全无效 —— 浏览器不支持 nonce,必须走 HTTP 响应头 - CSP 是兜底,不是替代:它拦不住已注入的 DOM 操作,也不解决输出编码缺失问题,三重防御(输入验证 + 输出编码 + CSP)缺一不可
真实项目里最容易被忽略的,是 SSR 和 CSR 渲染路径不一致带来的上下文错位:服务端生成的 nonce 值,若 hydration 过程中被 React/Vue 覆盖或丢失,CSP 就形同虚设;而模板引擎里一个 | safe 漏掉,可能让整页富文本沦为 XSS 入口。安全不是加个配置就完事,是每个数据流动节点都得确认编码方式与上下文匹配。


















