应用安全系列之四十八:JavaScript 原型污染----从原型链原理到 source–gadget–sink 攻击链

1. 背景与原理

1.1 原型链速览

JavaScript 是基于原型的语言。每个对象(null 除外)都有一个内部属性 [[Prototype]],指向它的原型对象。当访问一个对象的属性时,如果对象自身没有该属性,引擎会沿 [[Prototype]] 链向上查找,直到找到该属性或到达链的终点 Object.prototype(其 [[Prototype]] 为 null)。

在代码中,[[Prototype]] 通常通过 __proto__ 访问器暴露(非标准但被广泛支持),也可以通过 Object.getPrototypeOf() 获取。构造函数 Fn 的 prototype 属性是供 new Fn() 创建的实例使用的原型对象,而 fn.__proto__ 与 Fn.prototype 指向同一对象。

1.2 关键访问路径的关系

三者都能从不同角度触及同一个目标:Object.prototype。

表达式 含义 关键等价关系
obj.__proto__ 实例的原型引用 a.__proto__ === A.prototype
A.prototype 构造函数的原型对象 A.prototype.__proto__ === Object.prototype
A.prototype.constructor 指回构造函数本身 A.prototype.constructor === A

攻击者之所以能通过 __proto__、constructor、prototype 三个键名发起污染,正是因为它们构成了从任意对象"爬升"到 Object.prototype 的路径。

1.3 原型污染的本质

原型污染发生在应用使用攻击者可控的输入,通过递归合并、克隆或路径赋值,将属性写入 Object.prototype 的场景中。由于 Object.prototype 是几乎所有对象原型链的终点,一旦它被写入属性,该运行时中的每一个普通对象都会"继承"这个属性,即便这些对象与污染源毫无关系。

关键区分:污染本身不是漏洞终点 。单独写入一个无害属性(如 {}.foo = "bar")通常不直接造成危害。真正的风险在于应用其他位置的代码(gadget)读取了该属性,并将其用于敏感操作。

1.4 最小可复现示例(扩展)

以下是一个典型的不安全递归合并函数:

javascript

复制代码
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object' && typeof target[key] === 'object') {
      merge(target[key], source[key]);  // 递归进入嵌套对象
    } else {
      target[key] = source[key];        // 直接赋值
    }
  }
  return target;
}

攻击输入:

javascript

复制代码
const malicious = JSON.parse('{"__proto__": {"polluted": "yes"}}');
merge({}, malicious);

逐行分析:

  1. for...in 遍历 malicious 的键,第一个键是 "__proto__"。

  2. source["__proto__"] 的值是 {"polluted": "yes"},类型为 object;target["__proto__"] 是什么?对于 {}(空对象字面量),target.__proto__ 指向 Object.prototype,类型也是 object。

  3. 两个条件都满足,递归调用 merge(target.__proto__, source.__proto__)。注意:target.__proto__ 就是 Object.prototype 本身。

  4. 在递归调用中,key 为 "polluted",target(即 Object.prototype)上不存在 polluted 属性,于是执行 Object.prototype.polluted = "yes"。

  5. 污染完成。

验证:

javascript

复制代码
({}).polluted;  // "yes" —— 一个全新的空对象也“拥有”了 polluted 属性

这个示例揭示了原型污染的核心机制:递归合并函数将 __proto__ 当作普通键处理,没有阻止赋值路径穿透到 Object.prototype。


2. 常见攻击面与攻击方式(扩展)

2.1 典型入口(source)

不安全的合并/克隆函数是最主要的入口类别。已知受影响的具体组件包括:

  • lodash < 4.17.5 :defaultsDeep、merge、mergeWith 均存在原型污染,对应 CVE-2018-3721。需结合具体版本验证。

  • multiparty ≤ 4.2.3:处理 multipart 表单时,字段名若与原型属性冲突,可导致 DoS(CVE-2026-8161)。

  • content-security-policy-parser ≤ 0.5.0 :解析 CSP 指令时,策略名若为 __proto__ 可污染原型。

其他入口包括:查询参数解析(如 ?__proto__[key]=value)、JSON 请求体反序列化后合并、配置注入、路径式赋值(如 set(obj, "a.b.c", value))等。

2.2 客户端与服务端的差异

维度 客户端(浏览器) 服务端(Node.js)
污染源可见性 URL fragment/query、postMessage、JSON 输入 请求体、查询参数、配置文件
典型 gadget DOM XSS(innerHTML、script.src) DoS、逻辑绕过、RCE(通过模板引擎等)
检测难度 可在控制台直接验证 ({}).prop 需通过间接行为变化推断
持久性 单页面生命周期 进程级,影响所有后续请求

客户端示例:PortSwigger 实验展示了通过 ?__proto__[transport_url]=data:,alert(1); 污染 config.transport_url,使页面动态创建的 <script> 的 src 指向 data: URL,触发 XSS。

服务端 DoS 示例:multiparty 在解析字段名 __proto__ 时,尝试对继承的原型值调用 .push(),因该值不是数组而抛出未捕获的 TypeError,导致 Node.js 进程崩溃。

2.3 攻击链:source → property → sink

能污染不等于能利用。一个完整的原型污染攻击需要三个要素同时成立:

  1. Source(污染源):攻击者可控的输入进入了一个不安全的合并/赋值操作。

  2. Property(被污染属性) :写入 Object.prototype 的键名恰好是下游代码会读取的属性。

  3. Gadget(利用链) :应用(或依赖库)中存在一段代码,它读取该属性,并将其用于危险操作(如 innerHTML 赋值、eval、模板渲染、权限判断)。

如果缺少 gadget,即使成功污染,攻击也可能毫无实际影响。


3. 攻击可能造成的危害

除前文列出的分类外,以下是具有明确记录的影响实例:

  • DoS(进程崩溃):multiparty ≤ 4.2.3 中,单个精心构造的 multipart 请求即可使 Node.js 进程抛出未捕获异常并终止,无需认证。NocoDB 的连接测试端点也存在类似问题,污染后所有数据库写操作失败直至重启。

  • RCE(通过 gadget 链) :学术研究在 Node.js 核心 API 中发现了 11 个"universal gadgets",并在 NPM CLI、Parse Server、Rocket.Chat 等应用中手动利用了 8 个 RCE 漏洞。Kibana 的 CVE-2019-7609 是公开的著名案例。

  • 客户端 XSS :通过污染 transport_url、innerHTML、html 等属性,攻击者可在受害者浏览器中执行脚本,且 payload 可完全存在于 URL fragment 中,服务端日志和 WAF 均无法看到。

需注意 :RCE 结论高度依赖具体版本和运行时环境,需结合具体版本验证。


4. 检测与防御(OWASP 官方建议)

OWASP 原型污染防御速查表给出的核心措施:

优先使用 Map / Set :如果数据结构本质是键值对或集合,用 new Map() / new Set() 替代普通对象,从根本上消除原型链继承带来的风险。

必须使用对象时 :使用 Object.create(null) 创建无原型对象。这是最直接的隔离手段。

冻结原型 :Object.freeze(Object.prototype) 可阻止向原型添加新属性。但需注意兼容性:部分库会修改内置原型,冻结可能导致这些库异常。且冻结是浅层的,已有属性的值若为对象仍可变。

Node.js 运行时防护 :--disable-proto=delete 或 --disable-proto=throw 可移除或禁用 __proto__ 访问器。这是纵深防御措施,不能替代代码层修复 ,因为通过 constructor.prototype 路径的污染仍然可能。

静态检测思路:CodeQL/Semgrep 规则应建模 source(用户输入进入合并函数)到 sink(原型属性赋值)的污点传播路径。上述学术研究正是基于 CodeQL 实现了多阶段静态分析来发现 Node.js 库中的原型污染和通用 gadget。


5. 示例与练习(扩展)

5.1 客户端 DOM XSS 示例(教学用)

前置条件 :页面从 URL query 或 fragment 解析配置对象,并使用了不安全的合并逻辑;页面某处读取 config.transport_url 来设置 <script> 的 src。

污染 payload 构造思路 :?__proto__[transport_url]=data:,alert(document.domain)。

触发点 :页面代码中存在类似 script.src = config.transport_url || '/default.js' 的逻辑。由于 config 自身没有 transport_url,属性查找沿原型链到达被污染的 Object.prototype,读取到 data: URL,浏览器将其作为同源脚本执行。

注意:此示例仅用于理解 source→gadget→sink 链,实际利用需要目标页面具备对应 gadget。

5.2 服务端 DoS 示例(教学用)

前置条件:Node.js 服务使用 multiparty ≤ 4.2.3 处理文件上传或表单提交。

payload 构造思路 :发送一个 multipart/form-data 请求,包含一个字段名为 __proto__ 的字段。

触发点 :multiparty 在解析时对继承的 __proto__ 属性调用 .push(),因该属性不是数组而抛出 TypeError,未被捕获,进程崩溃。需结合具体版本验证是否仍受影响。


6. 常见误区

"只有 __proto__ 能污染" :错误。constructor.prototype 同样可以到达 Object.prototype。Node.js 的 --disable-proto 只移除了 __proto__,但通过 constructor 的路径仍然存在。

"原型污染必然导致 RCE":错误。RCE 需要存在可用的 gadget 链,且依赖运行时环境和版本。大多数原型污染的可利用性止步于逻辑绕过或 DoS,甚至完全没有实际影响。

"用了 Object.assign 就一定安全/不安全" :Object.assign 本身只做浅拷贝,不会递归。危险的是递归合并函数。但浅拷贝后若下游代码对嵌套对象做深度操作,仍需评估。

"冻结了 Object.prototype 就万事大吉":冻结确实能阻止添加新属性,但无法阻止修改已有属性的值(如果该值本身是可变对象)。且兼容性风险意味着生产环境需谨慎测试。

"客户端原型污染不重要,因为只影响自己" :错误。URL fragment 中的污染 payload 不经过服务端,可构造针对特定用户的 XSS,且日志和 WAF 完全不可见,检测难度极高。


7. 小结

原型污染的危险性不在于"写入了原型"这一动作本身,而在于污染属性恰好被下游代码信任并用于敏感操作 。防御的重心应放在:消除不安全的递归合并模式、使用无原型对象或 Map/Set、以及对 __proto__/constructor/prototype 键名的显式拦截。检测方面,静态污点分析在 Node.js 生态中已被证明有效,而客户端场景则需要结合运行时探针与代码审计。

相关推荐
嘟哩DuliDuli1 小时前
剧本节点输入归一化:文本来源、版本与下游引用
android·人工智能·安全·ai·软件工程
2601_962364973 小时前
Windows Hello 人脸解锁,指纹和红外摄像头哪个更安全?
windows·安全
郭庆汝3 小时前
(七)MCP+A2A之—— 沙箱模块开发:隔离环境运行本地工具确保Agent执行安全
安全
ynchyong3 小时前
LUKS 磁盘加密---你的磁盘安全专家
安全·磁盘加密·luks
国科安芯3 小时前
星载微控制器MCU功能安全与信息安全协同机制综述
网络·单片机·嵌入式硬件·安全·抗辐射
Liuruping044 小时前
建筑项目 VR 安全体验馆采购,如何评估厂商交付与售后能力?
安全·microsoft·vr
Black蜡笔小新4 小时前
中央厨房配餐看不见风险高?EasyCVR可视化监管方案设计
运维·安全·easycvr