题干与适用场景
一个 React 社区应用有三条渲染路径:
- 普通评论保存在数据库中,页面需要原样显示文字。
- 管理员公告允许使用有限的富文本:段落、强调、列表和 HTTPS 链接。
- 搜索页从
location.search读取q,在客户端显示“关于……的结果”。
审查发现普通评论和公告预览进入了 dangerouslySetInnerHTML 或 innerHTML,搜索提示也通过拼接 HTML 字符串渲染。请找出所有从数据源到注入点的路径,区分攻击数据的传播方式,按输出上下文 选择编码或净化方案,并增加能够降低漏网注入点影响的控制。
基础回答聚焦浏览器渲染和前端职责。服务端校验、鉴权与内容存储仍是必要边界,但它们不会让 危险的 DOM 注入点自动变安全。这道题的决定性能力是理解浏览器解析、框架逃生口、DOM 注入点、 CSP 以及可验证的渲染契约,因此归入 frontend。
存储型、反射型和 DOM 型是有用标签,但它们关注的维度不同。存储型和反射型描述攻击者数据 如何到达受害者;DOM 型描述客户端 JavaScript 如何把数据送进可执行的注入点。存储或反射的 数据也可能最终在 DOM 注入点执行,因此不能把三者当成完全互斥的分类。
面试官考察点
第一个信号是候选人能否先画数据流,再罗列安全响应头。强回答会识别数据库字段、URL 参数、 片段、postMessage 和第三方响应等数据源,也会识别 innerHTML、outerHTML、 insertAdjacentHTML、document.write、eval 和字符串定时器等注入点。核心问题是: 攻击者可控数据能否进入把字符串解释为标签、脚本或脚本 URL 的解析器?
第二个信号是能否根据数据的预期类型选择防御。普通评论属于文本,应进入文本注入点或普通 JSX 插值。公告确实需要 HTML,实体编码会破坏功能,因此必须在可信渲染边界前使用持续维护、 策略收窄的 HTML 净化器。URL 还要在正确输出上下文之外单独验证协议和目标。
第三个信号是能否说清框架边界。React 会转义普通字符串插值,dangerouslySetInnerHTML 则是 明确的逃生口。框架无法挽救未经处理的原始 HTML,也无法自动判断每个传给危险属性的 javascript: 或 data: URL 是否符合产品策略。
最后一个信号是分层验证。CSP 可以限制脚本执行,Trusted Types 可以让部分注入点拒绝普通 字符串,但它们都无法修复过宽的净化策略或直接返回原值的策略函数。强回答会删除不必要的 注入点、收窄唯一保留的注入点、部署浏览器侧控制,再用数据流审查和恶意样例证明结果。
回答前需要澄清的问题
- 哪些字段是文本,哪些字段确实需要 HTML? 如果全部是文本,删除所有原始 HTML 渲染。
如果公告需要格式,先精确定义允许的标签、属性和 URL,再选择净化器。
- 谁能发布公告? 仅管理员可写会降低暴露面,但内容仍应按不可信数据处理。管理员会话被盗、
导入工具失陷、迁移错误或 API 缺陷都可能写入恶意内容。
- 公告链接能否跳出站点? 基础方案只允许 HTTPS。若允许邮件、自定义协议、图片、视频或
内嵌页面,净化策略和隔离方式都要扩大。
- 当前在哪里净化? 写入时净化可以提前拒绝恶意输入,但渲染边界仍要保证:所有进入原始
HTML 注入点的值都经过当前策略处理。
- 需要支持哪些浏览器? CSP 具备广泛价值;Trusted Types 的支持范围和上线方式必须按真实
浏览器矩阵核对,未支持的客户端仍依赖安全注入点和 HTML 净化。
- 哪些第三方脚本必须保留? 脚本集合越小、审计越清晰,严格脚本策略越容易。标签管理器和
行内片段会改变 CSP 迁移方案,也会扩大成功 XSS 的影响。
- 怎样才算完成修复? 成功标准应包含回归测试、CSP 违规监控、净化器升级责任、策略变更
责任,以及发现新增注入点的方法。
30 秒回答框架
“我会先盘点所有不可信数据源和可执行注入点,再按数据的预期类型逐条修复。普通评论和搜索词 都是文本,用 React 普通插值或 textContent 渲染,完全避开 HTML 解析。公告确实需要有限 HTML,所以我会定义允许列表,让内容经过持续维护的净化器,并只保留一个经过审查的富文本 组件;链接还要校验协议。我会删除其他 innerHTML,先以 report-only 上线基于 nonce 或 hash 的 CSP,再逐步强制执行;浏览器矩阵支持时,用 Trusted Types 拒绝 DOM 注入点的原始字符串。 最后用存储数据、URL、畸形标签、事件属性、SVG 和脚本 URL 样例测试,同时确认允许的格式仍然 正常。”
分步骤深入解答
第一步:画出所有数据源到解析注入点的路径
先做一张小型数据流表,不要从背攻击字符串开始:
| 路径 | 攻击者可控数据源 | 当前注入点 | 预期类型 | 必要改动 | |---|---|---|---|---| | 评论卡片 | 数据库评论正文 | 原始 HTML 渲染 | 文本 | JSX 插值或 textContent | | 管理员公告 | 数据库公告正文 | 原始 HTML 渲染 | 有限 HTML | 允许列表净化后进入唯一注入点 | | 搜索提示 | location.search | HTML 字符串拼接 | 文本 | textContent 或 React 文本子节点 | | 公告链接 | 富文本中的 href | URL 属性 | HTTPS URL | 解析协议、验证后再保留属性 |
代码审查既要搜索直接注入点,也要搜索它们的封装。变量名叫 safeHtml 不能证明安全,必须追到 生成它的转换函数。还要检查 Markdown 渲染器、富文本编辑器、预览组件、含标记的国际化文案、 DOM 解析调用、SVG、模板工具、分析脚本,以及把 URL 或消息数据写入页面的代码。
路径确定后再分类:
- 恶意评论写入数据库并展示给其他用户,属于存储型传播。
- 请求参数被服务端立即放入响应,属于反射型传播。
- 客户端读取查询参数或片段,再送入
innerHTML,属于 DOM 型执行路径。
这些标签有助于评估暴露面和事件处置;修复选择仍由数据源、输出上下文和注入点决定。
第二步:让文本始终以文本渲染
普通评论和搜索提示不需要标记,直接使用 React 文本子节点:
function CommentBody({ body }: { body: string }) {
return <p>{body}</p>
}
function SearchSummary({ query }: { query: string }) {
return <p>关于“{query}”的结果</p>
}脱离 React 时使用文本注入点:
summaryNode.textContent = query浏览器此时把数据当文字,不会重新按 HTML 解析。不要用正则删除 script 字样后继续赋值给 innerHTML;HTML 的元素、属性、编码和畸形输入恢复规则很多,正则无法形成可靠解析边界。
上下文仍然重要。文本位置安全,不代表同一个值放进事件处理器、脚本块、CSS 规则或 URL 后 仍安全。避免把不可信值放入可执行上下文。确需写入安全属性时,固定属性名,并在框架属性或 setAttribute 前完成该属性所需的验证。
第三步:隔离唯一需要 HTML 的功能
公告需要保留有限格式,不能改成纯文本。先定义策略:
- 允许标签:段落、粗体、强调、无序和有序列表、列表项、链接。
- 允许属性:仅
href、title,以及渲染器统一添加的链接属性。 - 基础场景允许的链接协议:HTTPS。
- 禁止内容:脚本、事件属性、行内样式、内嵌页面、表单、SVG 和任意媒体。
然后把原始 HTML 注入点集中到一个组件:
function RichAnnouncement({ dirtyHtml }: { dirtyHtml: string }) {
const cleanHtml = sanitizeRichText(dirtyHtml, RICH_TEXT_POLICY)
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />
}sanitizeRichText 代表按上述策略配置、基于解析器且持续维护的净化器。OWASP 推荐 DOMPurify 作为可选实现之一。安全属性来自净化器及其配置,变量名或 TypeScript 类型本身不能提供防护。
净化应成为进入受审渲染边界前的最后转换。写入时净化可以增加一道检查,但单独依赖它会遗漏 旧数据、导入、迁移、备用 API 和策略变化。如果为了性能保存净化结果,应记录净化器和策略 版本,以便安全升级后重新处理历史内容。
净化后不要再用字符串拼接修改 HTML。后续添加链接、高亮或包装标签可能重新引入可执行结构。 可信 UI 应构建在原始 HTML 区域之外;确需修改最终内容时,再经过一次完整净化。
第四步:把 URL 当成结构化输入
HTML 净化必须覆盖链接属性,产品策略也要明确。把候选 URL 相对已知基地址解析,读取规范化后 的协议,只保留功能真正需要的协议。本题基础场景只允许 HTTPS。
对危险 URL 做编码,无法让其协议自动符合要求。一个值可以正确编码进 href,同时仍使用脚本 协议。URL 合法性和 HTML 属性上下文属于两项独立检查。外链在新标签页打开时,由渲染器统一 添加合适的关系属性;这能限制 opener 权限,但不能替代 XSS 防御。
完全禁止用户控制脚本 URL。eval、Function、字符串形式的 setTimeout、字符串形式的 setInterval 和动态脚本地址都不应接收不可信值。这些路径需要删除或改成固定映射,不能交给 通用净化器兜底。
第五步:把 CSP 作为第二道防线
如果仍有注入点漏网,CSP 可以限制脚本执行。优先通过响应头下发,并按应用真实资源设计策略。 简化方向如下:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{每个响应独立的随机值}';
object-src 'none';
base-uri 'none'nonce 必须不可预测,并且每个响应唯一,只给已批准脚本使用。稳定行内内容也可评估 hash 策略。 不要为了消除违规日志加入宽泛域名或 unsafe-inline,这会削弱策略边界。
先使用 Content-Security-Policy-Report-Only 收集违规,删除意外行内代码,再开启强制策略。 report-only 能提供上线证据,但不会阻止执行。CSP 始终属于纵深防御:允许的脚本仍拥有页面 权限,不同浏览器对功能的支持存在差异,过宽策略也可能让原漏洞继续执行。
第六步:用 Trusted Types 约束 DOM 注入
浏览器支持时加入以下 CSP 指令:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types app-rich-text开启后,受覆盖的 DOM 注入点会拒绝普通字符串。应用只创建指定名称的策略,该策略把 HTML 创建委托给获准净化器。原本静默发生的危险赋值会变成可见异常,新引入的原始字符串注入点也 更容易在测试和监控中暴露。
如果策略直接返回输入,控制就失去意义。宽泛的默认策略也可能把所有字符串自动转换,掩盖 旧路径。迁移期可以临时使用默认策略收集诊断信息,随后让调用方改用显式可信值,并删除不必要 的注入点和兼容路径。
Trusted Types 不能覆盖所有代码执行方式,旧浏览器也可能不支持强制机制。基线仍然是安全 渲染、收窄净化、URL 校验和移除可执行字符串 API。
第七步:降低渲染函数之外的影响
会话 Cookie 通常应设置 HttpOnly、Secure 和适当的 SameSite。HttpOnly 能阻止注入脚本 直接读取 Cookie,但活跃 XSS 仍可代表用户发请求或读取页面可访问数据。Cookie 属性只能降低 影响,不能关闭注入路径。
每个敏感操作都必须由服务端鉴权,即使前端隐藏了对应按钮。服务端边界按需要校验或净化输入, 存储的数据继续标记为不可信。第三方脚本拥有页面权限,应减少数量、限制到必要路由、审计更新, 并纳入 CSP 设计。
第八步:同时验证安全性和允许的格式
构建覆盖不同解析路径的样例集:
<img src=x onerror=alert(1)>
<a href="javascript:alert(1)">打开</a>
<svg onload=alert(1)></svg>
"><script>alert(1)</script>
畸形标签与混合字符编码在隔离测试环境中使用无破坏性的探针,不要在正常账号或生产环境执行真实攻击行为。验证:
- 普通评论逐字显示,且不会生成输入中包含的元素。
- 搜索参数在普通导航、编码 URL、历史记录和 hydration 后仍是文本。
- 公告保留允许标签,同时删除脚本、事件属性、样式、SVG、内嵌页面和危险 URL。
- 净化结果到达原始 HTML 注入点前没有再次被修改。
- 理解 CSP report-only 遥测后开启强制策略,故意加入的测试注入点会被阻止。
- Trusted Types 强制模式会拒绝原始字符串,只接受获准策略产生的净化结果。
- 现有富文本样例、链接、屏幕阅读器结构和复制粘贴行为仍正常。
增加新增注入点的静态审查、净化策略单元测试、每条渲染路径的集成测试和浏览器矩阵测试。 持续升级净化器;浏览器、框架、净化器、编辑器或策略变化时,重新运行恶意样例集。
高质量示范回答
“我会先识别不可信数据源和负责解析它们的浏览器 API。数据库评论、公告 HTML 和 location.search 中的 q 都是数据源;innerHTML、dangerouslySetInnerHTML 以及构造脚本 或脚本 URL 的 API 是要追踪的注入点。
评论和搜索提示是文本功能,我会把它们渲染成 React 文本子节点或赋值给 textContent,让 浏览器完全不把字符解释成标记。公告契约不同,它允许有限 HTML。我会为段落、强调、列表和 HTTPS 链接定义小型允许列表,让最终值经过持续维护、基于解析器的净化器,并只保留一个受审 的原始 HTML 组件。净化器同时删除事件属性、样式、SVG、内嵌页面和危险 URL 协议,净化后也 不再拼接标记。
恶意评论属于存储型传播,搜索参数路径属于 DOM 型。如果服务端把请求值立即写入 HTML 响应, 则属于反射型。这些标签帮助判断暴露方式,具体修复仍按输出上下文和注入点选择。
纵深防御方面,我会先用 report-only 上线基于 nonce 或 hash 的 CSP,处理违规后再强制执行。 浏览器支持时,要求脚本注入点使用 Trusted Types,只允许调用获准净化器的命名策略。我不会 把 CSP、HttpOnly Cookie 或直接返回原值的 Trusted Types 策略当成主要修复。
验证会覆盖存储数据、URL 数据、畸形标记、事件属性、SVG 和脚本 URL。我会断言文本路径不创建 元素、允许格式能保留、禁止内容被删除、Trusted Types 下原始字符串赋值失败,并确认强制 CSP 能挡住故意加入的测试注入点。最后持续更新净化器,把新增注入点纳入代码审查和静态检查。”
常见错误
- 所有值使用同一种转义 → 浏览器解析 HTML、属性、URL、CSS 和 JavaScript 的规则不同 →
避免把数据放入可执行上下文,并按准确注入点选择防御。
- 删除脚本标签后仍用
innerHTML显示纯文本 → 事件属性、畸形标记、SVG、URL 协议和解析器
行为仍可能利用 → 改用 JSX 文本或 textContent。
- 对富文本做实体编码 → 标记会原样显示,功能被破坏 → **HTML 是明确产品需求时,使用持续
维护且允许列表收窄的净化器。**
- 信任仅管理员可写的内容 → 账号失陷、导入、迁移和 API 缺陷都可能写入恶意标记 →
在渲染边界继续按不可信内容处理。
- 净化后继续拼接 HTML → 后续修改可能重新制造可执行结构 → **让净化成为进入受审注入点前
的最后转换。**
- 只用 HTML 编码校验
href→ 正确编码后的值仍可能使用不允许的协议 → **解析 URL,只允许
功能需要的协议和目标。**
- 认为 React 自动转义覆盖所有路径 → 逃生口和直接 DOM API 会绕过普通插值 →
盘点每条原始 HTML 和可执行字符串路径。
- 只依赖 CSP → 弱策略、获准脚本或不支持的功能都可能留下利用空间 →
先移除危险注入点,再把 CSP 作为附加屏障。
- Trusted Types 策略直接返回输入 → 浏览器得到可信对象,但转换过程不可信 →
限制策略名称,并把创建过程委托给受审净化器。
- 只确认一个 alert 样例失效 → 单一样例覆盖不了 URL 协议、畸形标记、替代元素和允许格式
回归 → 维护多样恶意样例和正常富文本样例。
追问及应对
追问一:旧系统有几百处 innerHTML,如何迁移?
先盘点注入点,按可到达的不可信数据源、用户暴露面和权限排序。优先替换所有纯文本路径,为 确实需要 HTML 的少量功能建立一个受审边界。以 report-only 或诊断模式部署 CSP 和 Trusted Types,找出静态搜索遗漏的运行时路径。临时默认 Trusted Types 策略可以记录旧调用,但不能 静默批准原始字符串;调用方迁移到显式策略后删除兼容路径。
追问二:公告必须支持图片和内嵌视频,方案如何变化?
信任契约会扩大。需要定义图片来源、URL 协议、尺寸、加载方式和隐私规则;图片代理可以减少 用户直接访问第三方。内嵌视频应限制为少量供应商,并使用只开放必要能力的 sandbox。净化策略、 CSP 指令、用户同意流程和测试集都要同步变化,任意内嵌页面、样式和供应商 HTML 不能直接进入 现有公告渲染器。
追问三:严格 CSP 让分析脚本和标签管理器失效,怎么办?
先在 report-only 中运行策略,按业务负责人和脚本用途分类每条违规。删除不用的脚本,把行内 片段改成获准外部模块,必要脚本按场景使用每响应 nonce 或稳定 hash。使用宽泛通配符或 unsafe-inline 会牺牲大量安全边界。若标签管理器可以注入任意脚本,应明确把它视为高权限 代码路径,并严格限制发布权限。
追问四:服务端已经净化公告,为什么还需要前端边界?
原始 HTML 组件无论在哪里执行转换,都需要可验证的输入契约。备用 API、旧数据、导入、缓存、 迁移或策略变化都可能绕过写入时假设。把注入点封装起来,只允许当前净化器输出进入,并对契约 做测试;复用已净化内容时记录策略版本。服务端检查减少坏数据进入系统,渲染边界防止未核验值 进入解析器。
追问五:如何调查生产环境中的 XSS 报告?
保存报告 URL、内容 ID、浏览器、CSP 报告和对应部署版本,避免在正常账号中执行载荷。在隔离 环境复现,追踪准确的数据源到注入点路径,并判断数据来自存储、请求还是第三方。先删除或关闭 脆弱渲染路径,按需清除恶意存储内容、轮换可能暴露的凭据,并检查暴露窗口内的敏感操作。 恢复功能前,把这类载荷加入回归样例集。