属性上下文未转义和ssr html结构不完整是两大高危隐患:前者导致属性注入,需用escapeattribute而非通用html转义;后者引发hydration失败,须确保输出仅为纯fragment且标签严格闭合。

服务端模板中属性值未转义导致的注入问题
动态 SSR 渲染时,模板引擎(如 EJS、Nunjucks、Vue SSR 的 v-html 或 {{ }})若直接将用户输入拼入 HTML 属性,会触发属性注入——比如 userInput = '" onmouseover="alert(1)" x="' 插入到 <div data-id="{{ userInput }}"> 中,最终生成 <code><div data-id="" onmouseover="alert(1)" x="">,浏览器解析后执行任意 JS。<p>关键点在于:属性上下文比文本上下文更危险,<code> 和 <code>> 不会触发解析,但引号闭合 + 事件属性却能绕过多数默认转义。
- 必须对所有插入到属性值中的变量做「属性级转义」,而非仅用通用 HTML 转义函数
- EJS 中应使用
(需自行实现或引入escape-html的escapeAttribute变体),不能依赖(它只做基础 HTML 实体转义) - Vue SSR 模板中,
:data-id="userInput"是安全的(Vue 自动调用escapeAttribute),但data-id="{{ userInput }}"是危险的(纯字符串插值,无转义) - Node.js 的
he.escape()默认不处理属性引号闭合,需手动加双引号包裹并调用he.escapeAttribute()(若存在)或自行正则替换:input.replace(/"/g, """).replace(/'/g, "'")
dangerouslySetInnerHTML / v-html 场景下的富文本净化边界
业务需要渲染用户提交的富文本(如评论、文章正文)时,dangerouslySetInnerHTML 或 v-html 是绕不开的入口,但它们本身不提供防护——是否安全,完全取决于你传进去的内容是否已被净化。
常见错误是「前端二次净化」:服务端吐出已清洗的 HTML,客户端又用 DOMPurify 再跑一遍。这不仅浪费 CPU,还可能因两次规则不一致导致标签被误删或保留。
- 净化必须且只能在服务端完成,客户端只负责渲染
- 不要用黑名单过滤(如移除
<script></script>),要用白名单:明确允许['p', 'br', 'strong', 'em', 'a']等标签,并限制a的href协议为http:、https:、/ - Node.js 推荐
sanitize-html,配置示例:allowedTags: ['p', 'br'], allowedAttributes: { a: ['href'] }, allowedSchemes: ['http', 'https'] - 若用 React,
dangerouslySetInnerHTML的__html字段必须来自服务端净化结果,绝不能传原始userInput
SSR 输出 HTML 结构完整性校验缺失引发的静默崩坏
SSR 返回的 HTML 字符串若包含多余 、 或未闭合标签,会导致客户端 hydration 失败——不是报错,而是光标错位、状态丢失、交互失效,且控制台无提示。
这类问题在模板嵌套、多层中间件注入、或构建工具自动插入 runtime 脚本时高频出现。
- 服务端返回的必须是「纯 fragment」:只含
<div id="root">...</div>或类似挂载点内容,不含顶层文档结构 - 检查最终响应的 HTML 源码(右键 → 查看页面源代码),确认没有重复的
、,且所有标签严格闭合 - Webpack/Vite 的
HtmlPlugin若启用inject: true,会往 SSR 输出里再塞 script,务必关掉,改用服务端显式注入 - 使用
renderToString后,用正则/]+>/g统计开闭标签数,或借助parse5做简单结构验证(非必须,但上线前建议跑一次)
nonce 与 CSP 在动态 SSR 中的链路断裂风险
启用 Content-Security-Policy: script-src 'nonce-xxx' 后,若服务端生成的 nonce 值未同步到响应头和对应 <script></script> 标签,浏览器会直接拒绝执行内联脚本,包括 SSR 所需的 hydration 脚本。
这不是配置错误,而是动态环境里三处值不一致:HTTP 响应头、HTML 中 <script nonce="xxx"></script>、以及客户端 JS 里读取 window.__nonce__ 的值(如果用了)。
- 每次请求必须生成新 nonce,且只生成一次,由同一逻辑写入响应头和模板
- 避免在 Nginx、CDN、框架中间件多层设置 CSP 头,任一层覆盖都会导致 mismatch
- 查看 Network → Headers,确认
Content-Security-Policy中的'nonce-xxx'与页面源码中<script nonce="xxx"></script>的值**逐字符一致**(注意大小写、无空格、无换行) - React/Vue SSR 中,若用
dangerouslySetInnerHTML或v-html动态插入脚本,这些脚本不会继承 nonce,必须显式传入当前请求的 nonce 值:<script nonce="${nonce}">...</script>
实际落地时,最易被忽略的是属性上下文转义和 fragment 完整性——它们不报错,但会让整个 SSR 流程在用户侧表现异常,排查成本远高于前置约束。











