JSON 工具箱

JSON 里的 XSS 是怎么发生的

结论:JSON 本身不会执行,XSS 发生在「把 JSON 当 HTML 用」的那一步。服务端拼页面、前端用 innerHTML、把 JSON 直接塞进 `<script>` 标签,是三个最常见的入口。

进阶约 4 分钟发布于 2026-10-30更新于 2026-10-30
JSON 数据引发 XSS 的三个入口与防护

结论:JSON 本身不会执行,防护要先堵住三个输出入口——服务端按上下文转义、前端用 textContent 代替 innerHTML、内联 JSON 时把 < 转成 \u003c。

{"name": "<img src=x onerror=alert(1)>"} 这段数据是合法的。它的危险性取决于你把它放到哪里。

入口一:服务端拼接 HTML

res.send(`<div>欢迎,${data.name}</div>`);

data.name 里的标签被浏览器当成 HTML 解析,脚本执行。JSON 在这里只是「装着攻击载荷的容器」,真正的问题是没有按 HTML 上下文转义。

修法:模板引擎默认转义(EJS 的 <%= %>、Vue 的 {{ }}、Django 的 {{ }}),或者显式调用转义函数。别用 <%- %>、v-html、|safe 这类「我知道它安全」的快捷方式去渲染用户数据。

入口二:前端 innerHTML

document.getElementById('profile').innerHTML = user.bio;   // 危险
document.getElementById('profile').textContent = user.bio; // 安全

textContent 把内容当纯文本插入,标签会原样显示;innerHTML 会解析标签。 React 的 {user.bio}、Vue 的 {{ user.bio }} 默认走文本节点,是安全的;一旦用 dangerouslySetInnerHTML 或 v-html,就等于主动开了这个口子。

如果确实要渲染富文本(Markdown 转 HTML),必须过一遍白名单净化(DOMPurify 之类),而不是直接信任后端。

入口三:内联进 <script>

服务端渲染时把数据塞进页面:

<script>
  window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>

JSON 里的 </script> 会提前闭合标签,后面的内容被当成新脚本执行。这是「JSON 内联」最容易忽略的一处。

修法:序列化后把危险字符转成 Unicode 转义:

JSON.stringify(data)
  .replace(/</g, '\\u003c')
  .replace(/>/g, '\\u003e')
  .replace(/&/g, '\\u0026');

这仍然是合法 JSON,解析出来的字符串一字不差,但不会再闭合标签。Next.js、Nuxt 这类框架的 __NEXT_DATA__、__NUXT__ 内联就做了类似处理。

顺带两个相邻风险

JSON 劫持(老浏览器)。 早期浏览器允许 <script src="api/user.json"> 加载 JSON 并执行,攻击者重定义 Array 构造函数就能读到数据。现代浏览器用 Object.defineProperty 保护,加上接口返回 Content-Type: application/json 和 X-Content-Type-Options: nosniff 就基本免疫。正确做法是接口返回不可执行的 JSON,别返回 var data = {...}。

JSONP 的遗留。 JSONP 本质是「用 script 标签跨域取数据」,回调函数名可控就意味着可以注入任意代码。新项目不要再用 JSONP,用 CORS。

一份检查清单

  • 所有把 JSON 字段写进 HTML 的地方,是否按上下文(HTML / 属性 / JS / URL)转义
  • 前端是否还有 innerHTML、v-html、dangerouslySetInnerHTML 直接吃接口数据
  • 内联 JSON 是否做了 < > & 转义
  • 富文本是否有白名单净化,而不是黑名单过滤 script
  • 响应头是否有 Content-Type: application/json 与 nosniff

常见问题

为什么合法的 JSON 数据也能触发 XSS?

JSON 本身只是文本,不会执行,合法性只说明它能被解析。危险发生在把它写进 HTML 或脚本上下文的那一步:标签被浏览器解析、</script> 提前闭合标签,脚本就会执行。问题不在数据格式,而在输出时没有按上下文转义。

前端渲染接口字段时该如何选择 textContent 与 innerHTML?

优先用 textContent,它把内容当纯文本插入,标签会原样显示。innerHTML 会解析标签,接口字段里带 <img onerror=...> 就会执行。React 的 {user.bio}、Vue 的 {{ user.bio }} 默认走文本节点,dangerouslySetInnerHTML 和 v-html 则是主动开这个口子。

内联 JSON 到 <script> 时应该如何转义?

需要。数据里的 </script> 会提前闭合标签,后面的内容被当成新脚本执行。序列化后把 <、>、& 分别替换成 \u003c、\u003e、\u0026,结果仍是合法 JSON,解析出来一字不差,但不会再闭合标签。

相关工具

延伸阅读