导读:本期聚焦于相泽南创作的《JavaScript为什么会被原型污染?如何检测和修复Prototype Pollution?》,敬请观看详情。JavaScript 中每个对象都有一条原型链,访问属性时会沿着 __proto__ 向上查找,直到 Object.prototype。这种机制本身是为了实现继承,但如果程序把用户可控的数据递归合并到对象中,攻击者传入 __proto__ 键就可能修改 Object.prototype,使整个运行环境里的普通对象凭空多出属性。原型污染的危险在于它不依赖传统注入点,也不需要突破内存边界,却可以改变框架的默认行为、绕过安全检查甚至导致拒绝服务。本文从原型链的工作方式出发,结合 merge、clone、deepExtend 等常见函数剖析污染过程,再通过实际代码演示如何利用 constructor.prototype 扩大影响,最后给出白名单校验、Object.freeze、安全合并策略等防御方法。理解这条攻击路径有助于在审计 Node.js 和前端项目时更早发现隐患。

原型污染(Prototype Pollution)并不是一种独立于 JavaScript 运行机制之外的新型攻击,而是直接利用对象原型链继承特性的一种注入方式。当一段代码无意中把用户可控的数据当作普通对象键值进行递归合并、复制或赋值时,攻击者可以通过传入 __proto__、constructor、prototype 等特殊键,改写全局对象 Object.prototype 上的属性。这种修改会立即影响当前 JavaScript 运行环境中的所有普通对象,哪怕这些对象没有显式定义对应属性,访问时也会从原型链上读取到被污染的值。理解原型污染需要从 __proto__ 与 prototype 的区别开始,并重点观察那些看似无害的对象合并操作。

JavaScript为什么会被原型污染?如何检测和修复Prototype Pollution?

一、原型链机制与污染成因

在 JavaScript 中,除了通过 Object.create(null) 创建的对象外,几乎每个对象都有一个内部原型。访问对象属性时,如果自身没有该属性,解释器会沿着原型链向上查找,最终可能到达 Object.prototype。这意味着一旦 Object.prototype 被添加了一个属性,所有普通对象在访问同名属性时都会命中这个值。正常开发中,我们通过对象字面量或 Object.defineProperty 添加属性,不会意外写进原型链,但递归合并函数却可能打开这个口子。

典型的不安全合并逻辑如下:

function unsafeMerge(target, source) {
  for (let key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      unsafeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

这段代码的问题在于,当 key 的值是 __proto__ 时,target[key] 并不会返回一个普通属性,而是返回目标对象的原型,也就是 Object.prototype。由于 Object.prototype 是真值,if (!target[key]) 判断不会创建新对象,而是直接递归调用 unsafeMerge(Object.prototype, source[key])。递归进入第二层后,普通键值被写入 Object.prototype,污染就此发生。攻击者只需要发送一段带有 __proto__ 键的 JSON,就能影响所有对象。

这里需要特别区分 __proto__ 和 prototype。__proto__ 是对象上的访问器属性,指向该对象的原型;prototype 则是构造函数上的属性,用来描述通过该构造函数创建的对象所共享的原型。两者虽然方向不同,但在污染链路中经常配合出现。如果只过滤 __proto__,攻击者还可以通过 constructor.prototype 绕到同一个目标。

二、攻击者如何利用原型污染

实际的攻击载荷并不复杂。很多应用会接收 JSON 格式的用户输入,然后把它合并到默认配置或应用对象中。攻击者可以提交如下 JSON:

let body = JSON.parse('{"constructor": {"prototype": {"isAdmin": true}}}');
unsafeMerge({}, body);
console.log({}.isAdmin); // true

这里 constructor 是普通对象从原型链上继承的属性,指向 Object 构造函数,而 Object 构造函数又拥有 prototype 属性。递归合并时,target['constructor'] 拿到 Object 函数,下一层拿到 Object.prototype,再下一层就把 isAdmin 写进了全局原型。这个例子说明仅靠检查 __proto__ 无法覆盖所有攻击路径。

一旦污染发生,影响面非常广。未显式定义的 isAdmin 属性会在所有普通对象上显示为 true,如果后端权限判断依赖类似 if (user.isAdmin) 的逻辑,攻击者就可能绕过权限校验。同理,攻击者可以覆盖 toString、hasOwnProperty 等原生方法,导致应用逻辑异常甚至拒绝服务。在前端场景中,原型污染还可能被用来干扰 DOM 过滤库、模板引擎或第三方脚本的行为,形成组合攻击。

三、常见漏洞场景与检测方法

历史上,lodash 的 merge、defaultsDeep,以及 jQuery 的 extend 等函数都曾经出现过原型污染相关漏洞。Node.js 应用中凡是使用深度合并、深拷贝、路径赋值等操作的模块,都可能成为入口。尤其是当合并函数的 source 参数直接来自 req.body、req.query 或外部 API 响应时,攻击者就拥有了控制键名的能力。

检测原型污染的方式之一是观察全局原型是否被意外修改。可以在应用启动后、处理请求前后执行如下检查:

function checkPollution(candidateKeys) {
  const obj = {};
  return candidateKeys.some(function (key) {
    return key in obj;
  });
}

console.log(checkPollution(['isAdmin', 'polluted'])); // true after pollution

更实用的做法是在开发阶段对关键函数做输入审计。看到 merge、extend、clone、deepClone、setPath 等名称时,重点确认它们是否使用了 for...in 遍历用户数据,是否对 __proto__、constructor、prototype 做了过滤。运行时也可以记录 Object.prototype 的键列表快照,在每次请求后对比是否有新增,虽然有一定性能开销,但在安全测试环境中很有价值。

静态扫描工具不一定能直接标记原型污染,但可以通过搜索危险合并函数和用户输入流向缩小范围。需要注意,漏洞不一定只存在于后端,浏览器端的深度复制库、URL 参数解析、消息订阅系统也可能受影响。只要 JavaScript 对象与攻击者可控的键名发生合并,就需要纳入检查范围。

四、防御原型污染的实践方案

防御的第一原则是不信任任何外部输入的键名。安全合并函数应当使用 Object.keys 获取自有属性,并显式屏蔽危险键:

function safeMerge(target, source) {
  if (typeof target !== 'object' || typeof source !== 'object') return target;
  const forbidden = new Set(['__proto__', 'constructor', 'prototype']);
  for (let key of Object.keys(source)) {
    if (forbidden.has(key)) continue;
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!Object.prototype.hasOwnProperty.call(target, key)) target[key] = {};
      safeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

这段代码同时做了几件事:使用 Object.keys 避免遍历原型链上的属性;使用 hasOwnProperty.call 避免继承属性被误用;用 Set 预先拦截 __proto__、constructor、prototype。如果业务场景允许,还可以在合并前递归清洗 JSON 对象,删除所有危险键,或者直接使用 Object.create(null) 创建无原型对象作为配置容器。

另一个有效的加固手段是冻结 Object.prototype。在应用启动阶段执行 Object.freeze(Object.prototype) 可以阻止后续对全局原型的修改。不过这种方式可能影响某些依赖原型扩展的旧代码,因此需要在测试环境充分验证。更推荐的做法是使用 Map 替代普通对象存储动态键值,因为 Map 的键不受原型链影响,天然免疫原型污染。

最后,升级到修复了已知漏洞的库版本也非常重要。例如 lodash 在较高版本中已经针对原型污染做了防护,但前提是不要在业务代码中自行实现不安全的 merge。开发团队还应当通过安全测试用例覆盖这些边界,例如用包含 __proto__、constructor.prototype 的 payload 对合并函数进行回归测试。原型污染的攻击成本很低,但修复和排查成本很高,所以在每次对象合并、克隆和赋值时都保持警惕,才能从根上降低风险。

原型污染Prototype PollutionJavaScript安全修改时间:2026-09-30 02:32:12

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0930/63648.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。