JSON 格式本身是安全的,但在实际应用中,不正确地处理 JSON 数据会带来严重的安全漏洞。本文从攻击者视角分析常见的 JSON 安全问题,并提供具体的防御代码示例,帮助开发者构建更安全的应用。
JSON 注入发生在服务端将用户输入直接拼接到 JSON 字符串中,而没有进行正确的转义处理。这可能导致攻击者篡改 JSON 结构,绕过身份验证或修改数据。
假设服务端代码直接拼接用户名:
// ⚠️ 危险代码 - 直接拼接用户输入
const username = req.body.username; // 攻击者输入: admin","role":"admin
const json = `{"username": "${username}", "role": "user"}`;
// 生成结果: {"username": "admin","role":"admin", "role": "user"}
// 某些解析器会取最后一个 role 值,但某些取第一个!
永远不要手动拼接 JSON 字符串,始终使用语言内置的序列化方法:
// ✅ 安全做法 - 用对象构建再序列化
const data = {
username: req.body.username, // 自动转义特殊字符
role: "user"
};
const json = JSON.stringify(data);
// Python 也一样
import json
data = {"username": username, "role": "user"}
json_str = json.dumps(data) # 自动安全处理
当你把 JSON 数据内嵌到 HTML 页面中(比如服务端渲染场景),需要格外小心 XSS(跨站脚本)攻击。
<!-- ⚠️ 危险:直接将 JSON 内嵌到 script 标签 -->
<script>
var data = {name: "用户输入的内容"};
// 如果 name 是 "</script><script>alert(1)</script>"
// 浏览器会误将其解析为 HTML,执行恶意脚本
</script>
// 方法1:在 JSON.stringify 后对特殊字符进行 HTML 转义
function safeJsonEmbed(data) {
return JSON.stringify(data)
.replace(/</g, '\\u003c') // < → \u003c
.replace(/>/g, '\\u003e') // > → \u003e
.replace(/&/g, '\\u0026') // & → \u0026
.replace(/'/g, '\\u0027'); // ' → \u0027
}
// HTML 中使用
<script>
var data = <?= safeJsonEmbed($data) ?>;
</script>
// 方法2:使用 data 属性传递(更推荐)
<div id="app-data" data-config='<?= htmlspecialchars(json_encode($data)) ?>'></div>
<script>
var data = JSON.parse(document.getElementById('app-data').dataset.config);
</script>
接收外部传入的 JSON 数据时,必须对其进行结构和类型验证,不能假设数据格式一定符合预期。JSON Schema 是验证 JSON 数据结构的标准方案。
// 定义 Schema:用户注册请求的格式要求
const userSchema = {
"type": "object",
"required": ["username", "email", "age"],
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 20,
"pattern": "^[a-zA-Z0-9_]+$" // 只允许字母数字下划线
},
"email": {
"type": "string",
"format": "email"
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
"role": {
"type": "string",
"enum": ["user", "editor"] // 只允许这两个值,防止权限提升
}
},
"additionalProperties": false // 禁止额外字段!
};
const Ajv = require('ajv');
const ajv = new Ajv();
const validate = ajv.compile(userSchema);
const valid = validate(req.body);
if (!valid) {
return res.status(400).json({
error: "数据格式不合法",
details: validate.errors
});
}
// 验证通过,安全地使用数据
additionalProperties: false——如果不设置这一项,攻击者可以传入任意额外字段,可能导致"属性污染"(Mass Assignment)漏洞,让攻击者意外修改不应该修改的字段(如 isAdmin、role 等)。// ⚠️ 错误:返回了密码哈希
{
"userId": 1001,
"username": "zhangsan",
"passwordHash": "$2b$10$abc123...", // 不该返回!
"email": "zhangsan@example.com",
"phone": "13812345678" // 完整手机号
}
// ✅ 正确:敏感字段脱敏或不返回
{
"userId": 1001,
"username": "zhangsan",
"email": "z***n@example.com",
"phone": "138****5678"
}
// 记录日志前,先清理敏感字段
function sanitizeForLog(data) {
const sensitiveKeys = ['password', 'token', 'secret', 'creditCard', 'ssn'];
const clone = JSON.parse(JSON.stringify(data));
function redact(obj) {
for (const key in obj) {
if (sensitiveKeys.some(k => key.toLowerCase().includes(k))) {
obj[key] = '[REDACTED]';
} else if (typeof obj[key] === 'object') {
redact(obj[key]);
}
}
}
redact(clone);
return clone;
}
// 安全地记录日志
logger.info('用户请求:', sanitizeForLog(req.body));
恶意用户可能发送巨型 JSON 数据(如嵌套 10000 层的对象),导致服务器内存耗尽或 CPU 占满(Billion Laughs 攻击的 JSON 变体)。
// Express.js:限制请求体大小
const express = require('express');
app.use(express.json({
limit: '1mb', // 限制请求体最大 1MB
strict: true, // 只接受对象和数组(不接受裸字符串/数字)
}));
// 同时限制嵌套深度(自定义中间件)
function limitJsonDepth(maxDepth) {
return (req, res, next) => {
function depth(obj, d = 0) {
if (d > maxDepth) throw new Error('JSON嵌套过深');
if (obj && typeof obj === 'object') {
for (const v of Object.values(obj)) depth(v, d + 1);
}
}
try { depth(req.body); next(); }
catch (e) { res.status(400).json({error: e.message}); }
};
}
app.use(limitJsonDepth(20));
无论 JSON 数据多么安全地构建,如果通过 HTTP(非 HTTPS)传输,中间人攻击者仍然可以读取和篡改内容。
// 防止响应被缓存(对敏感接口)
Content-Type: application/json
Cache-Control: no-store
X-Content-Type-Options: nosniff