原型链污染需三层防御:输入过滤(递归拦截__proto__等敏感键)、对象创建升级(用Object.create(null)替代{})、运行时加固(冻结Object.prototype并监控)。核心是不信任用户输入,禁用原型链作属性容器。

原型链污染无法靠单一手段根除,但能通过三层防御大幅压缩攻击面:输入过滤、对象创建方式升级、运行时加固。核心原则是——不信任任何用户输入,不把原型链当作普通属性容器来用。
输入层必须拦截敏感键名
所有外部数据(JSON 请求体、URL 参数、表单字段)进入业务逻辑前,必须递归扫描并移除含 __proto__、constructor、prototype 的键或嵌套路径。不能只检查顶层,例如 {"a": {"__proto__": {"x": 1}}} 或 {"x.__proto__.y": "z"} 都需匹配。
- 推荐正则:
/\b(__proto__|constructor|prototype)\b/i,对每个 key 和完整路径字符串做检测 - JSON.parse 后第一行代码就该执行清洗,而不是等到合并或赋值时才处理
- 避免“黑名单 + 仅顶层”这种常见错误,比如只写
if (key === '__proto__') continue对user['a.__proto__.b']完全无效
用 Object.create(null) 替代 {} 创建纯键值容器
当你需要一个纯粹的键值映射(如路由参数解析结果、缓存 Map、插件注册表),别再用 {}。它自带 Object.prototype,是污染的温床。
-
Object.create(null)创建的对象没有__proto__,也不继承toString、hasOwnProperty等方法,天然免疫原型污染 - 代价是不能直接调用
obj.hasOwnProperty(key),需改用Object.prototype.hasOwnProperty.call(obj, key) - 适合所有你本就不希望它有“行为”的场景,从根源上切断污染可能
合并与拷贝操作必须降级或加固
很多深拷贝和合并函数默认不校验原型键,尤其在处理不可信输入时极易触发污染。
- 避免使用
_.merge、_.set、_.defaultsDeep处理用户数据;若必须用,确保 lodash ≥ 4.17.21 - 优先改用
Object.assign({}, input)做浅拷贝——它不递归,天然跳过污染路径 - 需要深拷贝时,现代环境用
structuredClone();Node.js 可启用--disable-proto=throw参数强制拦截 - 手动实现递归逻辑时,遇到敏感键名直接跳过该分支,不进入下一层递归
运行时加一层兜底防护
静态防御可能被绕过,上线后仍需动态监控与快速响应。
- 服务启动时执行
Object.freeze(Object.prototype),阻止新增/修改属性(注意兼容性,部分老库会因此报错) - 定时检测
Object.prototype是否被新增非原生属性(遍历 keys 后排除已知方法) - 在日志中对
JSON.parse、req.body、fs.readFileSync等入口点添加字段扫描,命中敏感键立即告警 - 预发环境定期注入测试 payload,例如
{"__proto__": {"polluted": true}},验证({ }).polluted === true是否成立

















