应使用 reviver 函数过滤危险键、Object.create(null) 创建无原型对象、白名单赋值及 Schema 校验(如 Zod.strict())来阻断原型污染,而非依赖 hasOwnProperty 事后检查。

直接用 Object.prototype.hasOwnProperty.call(obj, key) 无法规避 JSON.parse 带来的原型污染风险——它只是安全地检测对象自身是否含有某个属性,但不阻止污染发生。真正危险的环节在解析后、使用前:当恶意 JSON(如 {"__proto__": {"isAdmin": true}})被 JSON.parse() 还原,再被不安全地合并(如 Object.assign({}, parsed) 或 _.merge),污染才落地。
要规避危险原型键,核心是在污染进入业务逻辑前切断路径,而不是靠 hasOwnProperty 事后检查。
在 JSON 解析后立即过滤敏感键
JSON.parse() 本身不执行代码,但它会如实还原 __proto__、constructor、prototype 等键。必须在解析完成后的第一时间递归清理:
-
使用
reviver函数在解析过程中跳过危险键:function secureJSONParse(str) { return JSON.parse(str, (key, value) => { if (key === '__proto__' || key === 'constructor' || key === 'prototype') { console.warn(`[secureJSONParse] Dropping dangerous key: ${key}`); return undefined; // 跳过该键,不写入结果 } return value; }); } -
对嵌套路径也要防护(如
"a.__proto__.b"不会被reviver捕获,需额外扫描):- 解析后递归遍历对象,用正则
/(__proto__|constructor\.prototype|prototype\.)/i匹配键路径; - 或改用更安全的解析库(如
destr),它内置预扫描和运行时键过滤。
- 解析后递归遍历对象,用正则
用 Object.create(null) 替代普通 {} 接收解析结果
即使 JSON 含有 __proto__,若你把解析结果赋给一个无原型的对象,污染就无法生效:
const raw = JSON.parse(maliciousJson);
const safeObj = Object.create(null); // 没有 __proto__,没有 hasOwnProperty 方法
// 手动拷贝可信任字段(白名单方式)
for (const key of ['id', 'name', 'email']) {
if (raw.hasOwnProperty(key)) {
safeObj[key] = raw[key];
}
}
⚠️ 注意:此时不能用 safeObj.hasOwnProperty,要改写为Object.prototype.hasOwnProperty.call(safeObj, key)
避免把解析结果直接传给不安全的工具函数
像 _.merge(target, parsed)、Object.assign(target, parsed)、_.set(obj, path, value) 这类操作,若 parsed 含 __proto__,会直接触发污染。应:
- 改用白名单赋值(只取已知字段);
- 或升级 lodash 到
4.17.21+并确认配置启用原型键拦截; - 或降级为
Object.assign({}, ...)(浅拷贝,不递归,天然避开嵌套污染)。
结合运行时 Schema 校验(推荐)
单纯过滤键名仍可能漏掉构造巧妙的攻击(如通过数组索引触发 0.constructor.prototype)。最佳实践是用 Zod、Joi 等做结构校验:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
}).strict(); // 拒绝任何未定义字段,包括 __proto__
const data = UserSchema.parse(JSON.parse(input)); // 自动剔除危险键 + 类型校验
不复杂但容易忽略:hasOwnProperty 是个好帮手,但它不是盾牌;真正的防御得落在解析后、使用前那几毫秒里。











