React 默认转义 JSX 插值这件事,让很多开发者误以为框架已经彻底解决了 XSS 问题。实际情况是,React 的保护机制像一道有明确边界的防火墙,墙内是安全的,但墙外那些需要直接操作 DOM、渲染富文本、处理 URL 的场景,恰恰是攻击者最喜欢下手的薄弱环节。下面直接拆解这些风险点,并给出可落地的防御方案。

dangerouslySetInnerHTML 是最大的敞口

这个 API 名字里就带着警告,但它依然是 React 应用中最常见的 XSS 源头。当后端返回一段 HTML 字符串需要直接渲染时,如果直接塞给 dangerouslySetInnerHTML,等于把执行权限完全交给了这段字符串。攻击者可以在内容中嵌入 script 标签或者事件处理器,浏览器会在插入 DOM 时执行它们。正确的做法是在赋值之前做一次消毒处理。推荐使用 DOMPurify 库,它会在浏览器环境中解析 HTML 并移除所有可执行脚本和白名单之外的属性。配置时要明确只允许安全的标签和属性,比如把 href 限制在 http、https 和 mailto 协议,彻底封死 javascript: 伪协议。即使使用了消毒库,也要保持库版本的更新,因为绕过技术一直在进化。

import DOMPurify from 'dompurify';

const sanitizedHTML = DOMPurify.sanitize(dirtyHTML, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
  ALLOWED_ATTR: ['href', 'title', 'target'],
  ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i
});
a 标签的 href 属性需要强制校验

用户资料里的个人网站链接、文章里的外部跳转,这些地方经常直接绑定 href 属性。如果不对协议做检查,攻击者可以注入 javascript:void(0) 或者更隐蔽的 javascript: 编码变体,点击链接触发脚本执行。校验逻辑要放在数据进入组件之前,用正则表达式或者白名单协议列表过滤。同时要考虑到相对路径和绝对路径的合法性,避免误伤正常的站内跳转。对于确实需要支持自定义协议的场景,要单独评估风险并限制使用范围。

const safeHref = (url) => {
  const allowedProtocols = ['http:', 'https:', 'mailto:'];
  try {
    const parsed = new URL(url);
    return allowedProtocols.includes(parsed.protocol) ? url : '#';
  } catch {
    return url.startsWith('/') || url.startsWith('#') ? url : '#';
  }
};
服务端渲染时的数据注入陷阱

React 在服务端通过 renderToString 输出 HTML 字符串时,如果初始状态数据是通过 window.__INITIAL_STATE__ 这类方式嵌入页面,需要特别注意序列化过程中的转义。JSON.stringify 本身不会转义 HTML 实体,如果状态数据中包含用户输入的恶意脚本标签,直接拼接进 script 标签内会导致浏览器解析时提前闭合标签并执行注入代码。解决办法是使用 serialize-javascript 这样的序列化库,它会把危险字符转义成 Unicode 安全序列,或者在写入时对特殊字符做 HTML 实体编码。

import serialize from 'serialize-javascript';

const html = `
  <script>
    window.__INITIAL_STATE__ = ${serialize(initialState, { isJSON: true })};
  </script>
`;
第三方脚本和 npm 包的供应链风险

npm 生态里任何一个依赖包的更新都可能引入恶意代码,这些代码运行在用户的浏览器上下文中,可以绕过 React 的所有防护直接操作 DOM。防御策略分两层:一是依赖锁定和完整性校验,使用 package-lock.json 和子资源完整性哈希确保加载的脚本未被篡改;二是实施内容安全策略,在 HTTP 响应头中配置 script-src 指令,严格限制可执行脚本的来源,禁止内联脚本和 eval。对于通过 script 标签动态加载的第三方服务,使用 nonce 或 hash 机制做细粒度控制。

CSS-in-JS 中的样式注入攻击

styled-components 和 Emotion 这类方案允许根据 props 动态生成样式,如果用户输入的颜色值、尺寸值未经校验直接插入到样式模板中,攻击者可以通过构造特殊的 CSS 值逃逸出当前样式上下文,注入带有 url() 或者 expression() 的危险规则。IE 时代的 expression 虽然已退出历史舞台,但 background-image: url(javascript:...) 在某些浏览器中仍可能触发行为。防御方法是把所有来自用户的样式值视为不可信输入,用白名单校验颜色格式、数值范围,或者直接使用设计系统预设的有限选项,不给用户自由输入样式参数的机会。

事件处理器中的回调注入

React 的事件系统是合成事件,直接写在 JSX 里的 onClick 等属性是安全的,因为它们是函数引用而不是字符串。但有些场景下开发者会动态构造事件处理逻辑,比如通过配置对象描述按钮行为,再在组件内部根据配置拼接执行。如果配置数据来自用户输入或者 URL 参数,攻击者可能注入任意函数调用。避免这种风险的原则是永远不把用户数据当作可执行代码,行为配置应该使用预定义的 action type 枚举,由组件内部 switch 分支来执行对应逻辑,而不是动态 eval 或者 new Function。

URL 参数和路由跳转的反射型攻击

React Router 的 useSearchParams 获取的查询参数如果直接渲染到页面中,就构成了反射型 XSS 的经典路径。虽然 React 的 JSX 会自动转义文本内容,但如果参数值被用于构造 href、src 等属性,或者被传入 dangerouslySetInnerHTML,转义保护就失效了。处理原则是:所有来自 URL 的数据在渲染前都要做类型校验和值过滤,对于需要在页面中回显的搜索关键词,用 textContent 方式赋值或者经过消毒处理。另外,对于重定向 URL 参数,要校验目标地址是否属于允许的域名白名单,防止钓鱼跳转。

文件上传中的 SVG 和 HTML 文件风险

用户上传头像、附件时,SVG 文件本质上就是 XML 文档,可以内嵌 script 标签和事件处理器。如果上传后的 SVG 直接以原始格式提供给其他用户访问,浏览器会解析并执行其中的脚本。防御方案是:对上传的 SVG 文件在服务端做一次净化,剥离所有脚本元素和事件属性,或者统一将用户上传的图片转换为位图格式如 PNG 重新编码。对于其他类型的文本文件,永远不要用内联方式预览,使用独立的源来提供下载,利用 Content-Disposition 头强制下载而非浏览器内打开。

postMessage 和跨窗口通信的监听漏洞

React 应用中如果有嵌入 iframe 或者被其他窗口引用的场景,通常会使用 postMessage 进行跨域通信。接收消息时如果没有校验消息来源的 origin,攻击者可以从恶意页面发送伪造消息,诱导接收端执行危险操作或者渲染攻击内容。每个 message 事件监听器内部,第一步必须是验证 event.origin 是否在可信域名列表中,第二步是校验 event.data 的结构和类型是否符合预期,拒绝任何不符合 schema 的消息。

内容安全策略的实战配置

CSP 是最后一道防线,即使前面的某个环节出现疏漏,合理的 CSP 策略也能阻止攻击代码的执行。对于 React 应用,推荐配置 script-src 'self' 并配合 nonce 机制允许必要的内联脚本,禁止 'unsafe-inline' 和 'unsafe-eval'。style-src 同理,避免内联样式成为攻击向量。object-src 设置为 'none' 阻止 Flash 等插件执行。base-uri 设置为 'self' 防止攻击者通过 base 标签劫持相对路径资源。CSP 的上线过程建议先使用 Report-Only 模式收集违规报告,逐步调整策略直到误报清零后再切换到强制模式。

自动化检测与开发流程集成

人工审查无法覆盖所有代码路径,需要把 XSS 检测嵌入到 CI/CD 流程中。ESLint 的 react/no-danger 规则可以直接阻断 dangerouslySetInnerHTML 的使用,迫使开发者显式处理消毒逻辑。同时配合静态代码扫描工具检查所有可能拼接 HTML 字符串的代码模式,标记出未经消毒处理就传入危险 API 的路径。在提交代码前用单元测试覆盖消毒函数的边界情况,包括空字符串、各种编码变体、嵌套标签等攻击 payload,确保消毒逻辑不会被绕过。

React 的 JSX 转义机制确实消灭了传统模板引擎中常见的大部分 XSS 漏洞,但它不是银弹。真正的安全来自于对每一个数据流入点的清醒认知:这个值从哪来,经过哪些处理,最终以什么形式进入 DOM。把消毒逻辑前置到数据层,把 CSP 策略部署在传输层,把自动化检查集成到开发流程,三层防线互为补充,才能把 XSS 风险控制在可接受的范围内。