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

结论: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,解析出来一字不差,但不会再闭合标签。
相关工具
- JSON 格式化与校验:看清载荷落在哪一层字段,便于定位渲染点
- JSON 处理中的安全风险清单:完整的安全检查项