FlawOpen

flawopen.com/漏洞/JavaScript 原型污染

JavaScript 原型污染

高危 CWE-1321 供应链与运行时
通俗解释 — 共享规则手册

想象一间工坊:每台新设备在自身设置无法回答问题时,都会查阅一本共享规则手册。如果零件管理员把攻击者控制的规则写进手册,其他设备可能会把它当作默认值。原型污染就是软件中的类似问题:不安全的动态属性写入可能更改对象原型,有时会触及 Object.prototype。影响不一定是全局的;还需要应用代码后来信任该继承属性。

目标:在服务器或浏览器运行、会合并不可信键或写入用户控制属性路径的 JavaScript
入口:不可信键,例如 __proto__,或通向不安全写入的 constructor.prototype 路径
影响:取决于可达的 gadget:逻辑变化、拒绝服务、XSS,或某些服务端场景中的代码执行;这些影响都不是自动发生的
修复:按 schema 或 allowlist 验证;不可信键使用 Map 或无原型字典

核心概念与子系统术语

Object.prototype
普通对象继承的共享原型。污染它可能影响许多对象,但并非所有 JavaScript 对象或所有 realm。
__proto__
普通对象上的旧式访问器,可读取或设置其原型。在 JSON 文本中,同样的拼写最初只是普通属性名,直到后续代码不安全地处理它。
递归深度合并
遍历嵌套键并写入另一个对象的复制函数。如果不可信键能选择原型相关属性或不安全路径,该函数就会有风险。
CWE-1321
对对象原型属性控制不当的修改。弱点是未受控的原型变更;后续影响取决于应用中可达的行为。

分步攻击流程

步骤 1

接收不可信属性

端点接受攻击者控制的对象键。仅解析含有自有 __proto__ 属性的 JSON 并不会更改原型。

步骤 2

执行不安全的动态写入

递归合并或路径 setter 沿这些键继续处理,并在未验证每个路径段的情况下动态写入。

步骤 3

更改目标或共享原型

根据目标和写入顺序,操作可能更改目标对象的原型,也可能触及共享原型(如 Object.prototype)。两者不同。

步骤 4

影响取决于 gadget

后续 gadget 在安全敏感操作中读取继承值。结果取决于该 gadget;不能直接假定会绕过认证或执行代码。

机制与根因

JavaScript 属性查找会沿原型链进行。存在漏洞的合并函数或路径 setter 可能允许不可信键(如 __proto__ 或 constructor.prototype)将写入导向原型。JSON.parse 生成的自有 __proto__ 字段本身并非污染;后续代码必须通过不安全赋值解释它。更改目标对象原型只影响该目标;修改 Object.prototype 会影响继承它的普通对象。只有另一个 gadget 使用了继承属性,才会产生安全影响。

utils.js(存在漏洞的递归合并)存在漏洞
// 存在漏洞:攻击者控制的键进入递归写入
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);
utils.js(按 allowlist 解析配置)已加固
// 已加固:仅复制此端点明确接受的字段
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;
}

攻击与利用过程

工程防御与预防措施

常见问题

JSON.parse() 本身会污染原型吗?

不会。JSON.parse() 会创建名为 __proto__ 的自有数据属性,不会调用继承的 setter。风险出现在后续合并或赋值操作触发该 setter 时。参见JSON.parse() 会导致原型污染吗?

Object.freeze(Object.prototype) 能修复吗?

它可以阻止写入被冻结的 Object.prototype 本身,但不能阻止 setter 改变单个对象的原型,也不能保护所有其他原型。它还可能破坏扩展内置对象的库。只能作为纵深防御,不能替代安全的数据处理。

TypeScript 能防护吗?

不能。类型在运行时会被擦除。声明为 Record<string, string> 的值仍可包含 __proto__ 键;应验证不可信键并安全构造对象。

浏览相关安全主题与事件分析:完整安全目录 →