flawopen.com/漏洞/JavaScript 原型污染
JavaScript 原型污染
想象一间工坊:每台新设备在自身设置无法回答问题时,都会查阅一本共享规则手册。如果零件管理员把攻击者控制的规则写进手册,其他设备可能会把它当作默认值。原型污染就是软件中的类似问题:不安全的动态属性写入可能更改对象原型,有时会触及 Object.prototype。影响不一定是全局的;还需要应用代码后来信任该继承属性。
__proto__,或通向不安全写入的 constructor.prototype 路径Map 或无原型字典核心概念与子系统术语
- Object.prototype
- 普通对象继承的共享原型。污染它可能影响许多对象,但并非所有 JavaScript 对象或所有 realm。
- __proto__
- 普通对象上的旧式访问器,可读取或设置其原型。在 JSON 文本中,同样的拼写最初只是普通属性名,直到后续代码不安全地处理它。
- 递归深度合并
- 遍历嵌套键并写入另一个对象的复制函数。如果不可信键能选择原型相关属性或不安全路径,该函数就会有风险。
- CWE-1321
- 对对象原型属性控制不当的修改。弱点是未受控的原型变更;后续影响取决于应用中可达的行为。
分步攻击流程
接收不可信属性
端点接受攻击者控制的对象键。仅解析含有自有 __proto__ 属性的 JSON 并不会更改原型。
执行不安全的动态写入
递归合并或路径 setter 沿这些键继续处理,并在未验证每个路径段的情况下动态写入。
更改目标或共享原型
根据目标和写入顺序,操作可能更改目标对象的原型,也可能触及共享原型(如 Object.prototype)。两者不同。
影响取决于 gadget
后续 gadget 在安全敏感操作中读取继承值。结果取决于该 gadget;不能直接假定会绕过认证或执行代码。
机制与根因
JavaScript 属性查找会沿原型链进行。存在漏洞的合并函数或路径 setter 可能允许不可信键(如 __proto__ 或 constructor.prototype)将写入导向原型。JSON.parse 生成的自有 __proto__ 字段本身并非污染;后续代码必须通过不安全赋值解释它。更改目标对象原型只影响该目标;修改 Object.prototype 会影响继承它的普通对象。只有另一个 gadget 使用了继承属性,才会产生安全影响。
// 存在漏洞:攻击者控制的键进入递归写入
function merge(target, source) {
for (const key in source) {
const value = source[key];
if (value && typeof value === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], value);
} else {
target[key] = value;
}
}
return target;
}
// JSON.parse 会创建自有 __proto__ 数据属性;风险来自不安全的合并。
const input = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, input);
// 已加固:仅复制此端点明确接受的字段
const PROFILE_KEYS = new Set(['displayName', 'theme']);
function parseProfile(input) {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new TypeError('profile must be an object');
}
const profile = Object.create(null);
for (const key of Object.keys(input)) {
if (!PROFILE_KEYS.has(key)) continue;
const value = input[key];
if (typeof value !== 'string' || value.length > 80) {
throw new TypeError('invalid profile field');
}
profile[key] = value;
}
return profile;
}
攻击与利用过程
- 1输入:请求带有意外对象键,例如
{"__proto__": {"admin": true}}。单独解析 JSON 不会更改原型。 - 2不安全写入:通用合并函数或路径 setter 在没有 allowlist 的情况下处理该键。
- 3触及原型:具体写入可能更改目标原型或触及共享原型,取决于代码路径。
- 4影响 gadget:只有后续代码在敏感决策中信任继承属性时,污染才会影响该决策。应测试实际 gadget 和授权路径。
工程防御与预防措施
- ✓优先使用 schema allowlist:仅用端点允许的字段构造目标对象。如果必须使用通用路径写入器,则在每一层拒绝与原型有关的路径段。
- ✓使用安全字典:任意用户键存入
Map或Object.create(null)字典;传给期望普通对象的 API 前先验证。 - ✓检查自有属性:安全敏感的存在性检查使用
Object.hasOwn(record, key);不要让继承的默认值授权操作。 - ✓将运行时加固作为纵深防御:Node.js
--disable-proto=throw会移除一条路径,但不能阻止constructor.prototype。冻结内置对象可能破坏 polyfill 和依赖;应评估兼容性,且不能用它替代安全写入。
常见问题
不会。JSON.parse() 会创建名为 __proto__ 的自有数据属性,不会调用继承的 setter。风险出现在后续合并或赋值操作触发该 setter 时。参见JSON.parse() 会导致原型污染吗?
它可以阻止写入被冻结的 Object.prototype 本身,但不能阻止 setter 改变单个对象的原型,也不能保护所有其他原型。它还可能破坏扩展内置对象的库。只能作为纵深防御,不能替代安全的数据处理。
不能。类型在运行时会被擦除。声明为 Record<string, string> 的值仍可包含 __proto__ 键;应验证不可信键并安全构造对象。
- MDN — JavaScript 原型污染
- OWASP — 原型污染防护指南
- CWE-1321 — 对对象原型属性控制不当的修改
- MDN — Object.prototype.__proto__ 访问器