原型污染防御需在数据进入业务逻辑前拦截敏感键名,包括__proto__、constructor、prototype等,须递归校验嵌套路径,优先用object.create(null),慎用冻结原型。

直接拦截 __proto__、constructor、prototype 等敏感键名
原型污染的入口几乎都始于这几个键名被当作普通属性写入。只要在对象合并、赋值、解析前做一次字符串级拦截,就能堵住绝大多数攻击链。
- 不能只检查顶层键:攻击者常用
a.__proto__.b或constructor.prototype.exec这类嵌套路径绕过校验,需对整个 key 路径做分段匹配或正则扫描(例如/\b(__proto__|constructor|prototype)\b/i) - JSON 解析后立即过滤:不要等数据进业务逻辑才处理,
JSON.parse()后第一件事就是递归遍历并删除含敏感路径的键 - 避免“黑名单 + 仅顶层”这种常见错误:很多代码只写
if (key === '__proto__') continue,但对user['x.__proto__.y'] = ...完全无效
用 Object.create(null) 替代普通空对象字面量
当你需要一个纯粹的键值容器(比如缓存 Map、配置映射表、请求参数白名单校验器),别再用 {}。它自带 Object.prototype,是污染的温床。
-
Object.create(null)创建的对象没有__proto__,也没有toString、hasOwnProperty等方法,天然免疫原型污染 - 代价是:不能直接调用
obj.hasOwnProperty(key),得改用Object.prototype.hasOwnProperty.call(obj, key);也不能用obj.toString(),需显式转成字符串 - 适合场景:路由参数解析结果、用户上传 JSON 的中间存储、插件注册表——所有你本就不想让它继承任何行为的地方
升级或替换有风险的深拷贝/合并函数
_.merge、_.defaultsDeep、_.set 在 lodash 版本中默认不校验原型键,且修复版本本身也经历过多次补丁(如 <code>4.17.12 修了 __proto__,但 4.17.21 才彻底覆盖 constructor.prototype 场景)。
- 优先降级为安全原生方案:用
Object.assign()做浅拷贝(不递归,自然避开污染);现代环境可用structuredClone()(支持循环引用和内置类型,且不走原型链) - 若必须深合并,自己实现时加硬性守卫:遇到
__proto__、constructor、prototype直接跳过该分支,不递归也不赋值 - 警惕“已修复”幻觉:某些团队升级到
lodash@4.17.15就以为高枕无忧,但实际4.17.21才是 Elastic 官方在 CVE-2025-25012 分析中确认的稳定基线
冻结关键原型(仅限沙箱或高安全场景)
Object.freeze(Object.prototype) 是最彻底的防御动作,但它不是银弹,而是一把双刃剑。
- 它能阻止新增/修改/删除属性,包括攻击者注入的
exec、isAdmin等,实测在 Kibana 漏洞复现中可完全阻断利用链 - 副作用明显:部分依赖原型扩展的库(如旧版 Backbone、某些 polyfill)会静默失败或抛错;
Math、Date等原型冻结后,其静态方法调用可能受影响 - 真正可行的用法:只在前端沙箱环境、服务端无状态 worker、或 CI 构建脚本这类可控上下文中启用;生产 Web 应用主流程中慎用
原型污染的隐蔽性在于它不报错、不中断、甚至不触发 console.warn,只有当某个看似无关的 obj.exec() 突然执行系统命令时,你才会意识到几小时前那个 JSON 上传已经污染了整个运行时。防御的关键不是堆砌方案,而是把校验点卡在数据进入业务逻辑前的最后一道门——那里,才是污染真正开始的地方。










