FlawOpen

flawopen.com/原型污染/JSON.parse()

JSON.parse() 会导致原型污染吗?

CWE-1321:原型污染参考指南
简答

不会。JSON.parse() 会创建名为 __proto__ 的自有数据属性;解析过程不会调用继承的 setter,也不会改变原型。只有后续代码通过 setter 复制该键、递归合并不可信键,或写入攻击者可控的对象路径时,才可能产生风险。

不安全复制 — 仅限局部目标
// JSON.parse 创建数据;不会污染原型。
const input = '{"__proto__":{"isAdmin":true}}';
const parsed = JSON.parse(input);

// Object.assign 会对目标执行 Set。此处只会改变
// 这个局部演示对象的原型。
const copied = Object.assign({}, parsed);

Object.getPrototypeOf(copied).isAdmin; // true
Object.prototype.isAdmin;              // undefined
安全处理 — 只构造已知结构
// 解析后只构造应用明确接受的字段。
const input = '{"theme":"dark","locale":"en","__proto__":{"isAdmin":true}}';
const parsed = JSON.parse(input);
const config = {
  theme:  parsed.theme,
  locale: parsed.locale,
};

// 不可信字典键应避免触发继承的 setter。
const options = Object.assign(Object.create(null), parsed);

为什么 JSON.parse() 不是污染源

JSON 键与对象字面量中的原型 setter 是两种不同操作。JSON 解析会在新值上定义自有属性,不会执行 obj.__proto__ = value。后续操作可能不同:Object.assign(target, source) 会向目标执行赋值,因此自有的 __proto__ 键可能触发目标继承的 setter。这会改变目标对象的原型;单凭这一点并不会修改 Object.prototype。

执行 const o = JSON.parse('{"__proto__":{"a":1}}') 后,Object.hasOwn(o, "__proto__") 为真,而 Object.prototype.a 仍未定义。后续复制到普通目标时,目标的原型可能改变。对象展开会创建自有属性而不调用 setter,但不能防护不安全的递归合并或路径写入。

真正的风险出现在哪里

解析后的 JSON 是不可信数据,并非已污染的原型。风险取决于后续写入点:递归合并、路径 setter,或向普通对象赋值的复制循环。易受攻击的递归合并可能沿着 __proto__ 或 constructor.prototype 写入共享原型;单层复制则可能只改变目标对象。判断影响前应追踪具体辅助函数和目标对象。

审查不可信键写入的位置,而不只是 JSON 解析位置:深度合并函数、对象路径 setter,以及执行赋值的 for...in 循环。用户可控键应拒绝 __proto__、constructor 和 prototype;优先按已验证的结构重建对象,或使用无原型字典。

常见问题

是否应该始终用 reviver 删除 __proto__?

如果应用不需要该键,reviver 可以在解析时将其删除。这属于纵深防御,不能替代对不安全合并和路径写入的修复;constructor.prototype 和非 JSON 输入也需要考虑。

Express 或 body-parser 能保护我吗?

请求体解析器只会生成 JavaScript 值,并不会让后续复制或合并自动变安全。请检查实际使用的解析器和后续操作。有些查询字符串解析器还会根据方括号语法构建嵌套对象,这是另一条输入路径。

JSON.stringify() 会导致原型污染吗?

不会。序列化只读取值并生成 JSON 文本,不会改变对象原型。如果文本随后被解析并传入不安全的处理函数,风险才可能再次出现。

参考资料

另请参阅 JavaScript 原型污染:成因与防护 JSON.parse() 比 eval() 更安全吗? lodash.merge() 能防止原型污染吗? Object.assign() 与原型污染:哪些用法安全?