原型链污染本质是攻击者通过可控输入向object.prototype写入属性,导致所有对象继承恶意行为;其发生源于javascript属性查找时沿__proto__向上遍历至object.prototype,一旦被注入属性(如admin、tostring),所有对象均会意外继承;常见触发场景包括解析含{"__proto__":{}}的json后合并、使用有缺陷的lodash.merge等深拷贝函数、手动递归赋值未过滤敏感键名;防御需组合object.freeze(object.prototype)、object.create(null)、输入过滤敏感键、升级安全版本依赖及采用structuredclone等安全api。

原型链污染本质是攻击者通过可控输入,向 Object.prototype 写入属性,导致所有对象继承恶意行为。它不是“代码写错了”,而是执行环境被悄悄改写了底层规则。
污染是怎么发生的?
关键在于 JavaScript 的属性查找机制:访问一个对象上不存在的属性时,引擎会顺着 __proto__ 一路向上找,直到 Object.prototype。一旦这里被注入了属性(比如 toString、hasOwnProperty 或自定义字段),所有对象都会“意外拥有”它。
常见触发场景有:
- 把用户提交的 JSON(如
{"__proto__": {"admin": true}})直接JSON.parse()后合并进配置对象 - 用存在缺陷的深拷贝/合并函数(如旧版 lodash.merge)处理不可信数据
- 手动递归赋值时,没过滤
__proto__、constructor、prototype这类敏感键名 - 第三方库在初始化时偷偷修改原生原型(例如覆盖
Array.prototype.map)
Object.freeze 是怎么起作用的?
Object.freeze() 不是“加个锁”,而是让对象进入一种不可变更状态:无法添加、删除、修改任何自有属性,也无法改变属性的可配置性或可写性。当它作用于 Object.prototype 及其他原生原型时,就从根源上堵住了污染入口。
实际操作要点:
- 必须在任何第三方脚本执行前调用,通常放在 HTML 中第一个
<script></script>的最顶部 - 需要冻结多个原型:
Object.freeze(Object.prototype)、Object.freeze(Array.prototype)、Object.freeze(Function.prototype)、Object.freeze(String.prototype)、Object.freeze(Boolean.prototype)、Object.freeze(Date.prototype) -
Math不是构造函数,其prototype不存在,但Math对象本身也可 freeze(非必需) - 严格模式下,尝试污染会立即抛出
TypeError;非严格模式下静默失败,同样能阻断攻击
光靠 freeze 还不够,得配合其他习惯
冻结原型是硬防护,但业务层仍需主动规避风险:
- 配置对象别用
{},改用Object.create(null)——它没有原型链,天然免疫污染 - 解析外部数据后,不要直接
Object.assign(target, input),先做白名单校验或键名过滤(删掉__proto__等) - 升级依赖:确认 lodash ≥ 4.17.21、hoek ≥ 5.0.3 等已修复版本
- 现代环境优先用
structuredClone()替代不安全的深拷贝逻辑
冻结不是万能解药,但它是最简单、最有效的第一道防线。真正健壮的防御,是冻结 + 输入过滤 + 安全 API 选择三者结合。











