各位观众老爷,晚上好! 咳咳,今天咱们聊点刺激的——JavaScript 原型污染 (Prototype Pollution)。这玩意儿就像个隐藏的定时炸弹,平时你可能根本感觉不到它的存在,但一旦引爆,那可就热闹了,轻则页面崩坏,重则直接被人黑进服务器,想想都后背发凉。
咱们今天就来扒一扒这原型污染的底裤,看看它到底是怎么作妖的,以及咱们该如何“防狼”。
一、什么是原型污染?
别被“原型”这两个字唬住,其实概念很简单。在 JavaScript 里,每个对象都有一个原型 (prototype)。你可以把它想象成一个“老祖宗”,对象会继承老祖宗的属性和方法。
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log("Hello, my name is " + this.name);
};
const john = new Person("John");
john.greet(); // 输出: Hello, my name is John
console.log(john.__proto__ === Person.prototype); // 输出: true
在这个例子里,Person.prototype
就是 john
这个对象的原型。john
可以直接调用 greet
方法,因为它继承了原型上的方法。
原型污染,简单来说,就是 恶意修改了 JavaScript 对象的原型,导致所有基于该原型创建的对象都受到影响。 这就像往你家的自来水里投毒,全小区的人都得遭殃。
最容易被污染的就是 Object.prototype
,因为 JavaScript 里几乎所有对象都直接或间接继承自它。 污染了 Object.prototype
,基本上就等于污染了整个 JavaScript 世界。
二、原型污染是怎么发生的?
原型污染通常是因为代码中存在一些不安全的、允许修改对象属性的操作,而且这些操作的属性名又是可以由用户控制的。 听起来有点绕,咱们举几个栗子:
1. 深度合并 (Deep Merge)
深度合并是一种常见的操作,用于将多个对象合并成一个对象,如果遇到嵌套的对象,会递归地合并。但如果深度合并的逻辑写得不够严谨,就可能导致原型污染。
function deepMerge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
const obj = {};
const maliciousPayload = JSON.parse('{"__proto__":{"isAdmin":true}}');
deepMerge(obj, maliciousPayload);
console.log({}.isAdmin); // 输出: true
在这个例子里,maliciousPayload
包含一个 __proto__
属性,它的值又是一个对象,这个对象设置了 isAdmin
为 true
。 deepMerge
函数没有对 __proto__
属性进行过滤,导致它被合并到 Object.prototype
上,从而污染了所有对象。
为啥是 __proto__
?
__proto__
是一个非标准的属性,用于访问对象的原型。虽然它已经被标准化,但它仍然是原型污染的主要入口点。 在一些老的浏览器或环境中,可能还会使用 constructor.prototype
来进行原型污染。
2. 点号赋值 (Dot Notation)
如果属性名可以由用户控制,并且直接使用点号赋值,也可能导致原型污染。
function setProperty(obj, key, value) {
obj[key] = value;
}
const obj = {};
const key = '__proto__.isAdmin';
const value = true;
setProperty(obj, key, value);
console.log({}.isAdmin); // 输出: true
在这个例子里,setProperty
函数直接使用 obj[key] = value
来设置属性,如果 key
是 __proto__.isAdmin
,就会导致原型污染。
3. 其他情况
除了深度合并和点号赋值,还有一些其他情况也可能导致原型污染,例如:
- 反序列化不安全的 JSON: 如果 JSON 字符串中包含
__proto__
属性,并且没有进行过滤,就可能导致原型污染。 - 模板引擎: 如果模板引擎允许用户控制属性名,并且没有进行安全检查,也可能导致原型污染。
- 第三方库: 一些第三方库可能存在原型污染漏洞,如果使用了这些库,也可能受到影响。
三、原型污染的危害
原型污染的危害可大可小,取决于污染的内容和应用程序的逻辑。
- 拒绝服务 (Denial of Service, DoS): 可以通过污染原型,导致应用程序抛出异常或进入死循环,从而拒绝服务。
- 权限提升 (Privilege Escalation): 可以通过污染原型,修改应用程序的权限控制逻辑,从而获得更高的权限。
- 远程代码执行 (Remote Code Execution, RCE): 在某些情况下,可以通过污染原型,执行任意代码。 例如,如果应用程序使用了
Function.prototype.apply
或Function.prototype.call
,并且参数可以由用户控制,就可能通过原型污染来执行任意代码。 - 信息泄露 (Information Disclosure): 可以通过污染原型,访问或修改应用程序的敏感数据。
举个权限提升的栗子:
假设我们有一个简单的权限验证系统:
const user = {
username: "guest",
isAdmin: false
};
function checkAdmin(user) {
if (user.isAdmin) {
console.log("You are an admin!");
} else {
console.log("You are not an admin.");
}
}
checkAdmin(user); // 输出: You are not an admin.
// 恶意攻击者进行原型污染
Object.prototype.isAdmin = true;
checkAdmin(user); // 输出: You are an admin!
由于 Object.prototype
被污染,所有对象现在都拥有 isAdmin
属性,并且值为 true
。 这就导致了权限提升,原本不是管理员的用户现在也被认为是管理员了。
四、如何防御原型污染?
防止原型污染是一个需要重视的问题,我们需要从多个方面入手。
1. 避免不安全的属性赋值
-
使用
Object.create(null)
创建对象:Object.create(null)
创建的对象没有原型,因此不会受到原型污染的影响。const obj = Object.create(null); obj.__proto__ = { isAdmin: true }; // 无效 console.log(obj.isAdmin); // 输出: undefined
注意: 使用
Object.create(null)
创建的对象没有继承任何属性和方法,包括toString
、valueOf
等,因此在使用时需要小心。 -
使用
Object.freeze()
冻结对象:Object.freeze()
可以冻结对象,使其无法被修改。const obj = { name: "John" }; Object.freeze(obj); obj.name = "Jane"; // 静默失败 (在严格模式下会抛出 TypeError) console.log(obj.name); // 输出: John
注意:
Object.freeze()
只能冻结对象的直接属性,如果对象包含嵌套的对象,则嵌套的对象仍然可以被修改。 -
使用
Object.seal()
封闭对象:Object.seal()
可以封闭对象,使其无法添加新的属性,但可以修改已有的属性。const obj = { name: "John" }; Object.seal(obj); obj.name = "Jane"; // 有效 obj.age = 30; // 静默失败 (在严格模式下会抛出 TypeError) console.log(obj.name); // 输出: Jane console.log(obj.age); // 输出: undefined
注意:
Object.seal()
只能封闭对象的直接属性,如果对象包含嵌套的对象,则嵌套的对象仍然可以被修改。 -
使用
Object.defineProperty()
定义属性:Object.defineProperty()
可以精确地控制属性的行为,例如设置writable
、configurable
和enumerable
标志。const obj = {}; Object.defineProperty(obj, "isAdmin", { value: false, writable: false, configurable: false, enumerable: true }); obj.isAdmin = true; // 静默失败 (在严格模式下会抛出 TypeError) console.log(obj.isAdmin); // 输出: false
-
避免使用用户可控的属性名进行赋值: 尽量避免使用
obj[key] = value
这种方式进行赋值,特别是当key
可以由用户控制时。 如果必须使用,需要对key
进行严格的验证和过滤。
2. 过滤敏感属性
-
过滤
__proto__
、constructor
和prototype
属性: 在处理用户输入或外部数据时,需要对__proto__
、constructor
和prototype
属性进行过滤,防止它们被用于修改原型。function sanitize(obj) { const newObj = {}; for (const key in obj) { if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') { newObj[key] = obj[key]; } } return newObj; } const maliciousPayload = JSON.parse('{"__proto__":{"isAdmin":true}, "name": "John"}'); const sanitizedPayload = sanitize(maliciousPayload); console.log(sanitizedPayload.__proto__); // 输出: undefined console.log(sanitizedPayload.name); // 输出: John
-
使用
Object.hasOwn()
或hasOwnProperty()
检查属性是否是对象自身的属性: 在访问属性之前,可以使用Object.hasOwn()
或hasOwnProperty()
检查属性是否是对象自身的属性,而不是从原型链上继承的。const obj = { name: "John" }; Object.prototype.isAdmin = true; console.log(Object.hasOwn(obj, "name")); // 输出: true console.log(Object.hasOwn(obj, "isAdmin")); // 输出: false console.log(obj.hasOwnProperty("name")); // 输出: true console.log(obj.hasOwnProperty("isAdmin")); // 输出: false
注意:
Object.hasOwn()
是 ES2022 新增的方法,如果需要兼容旧版本的浏览器,可以使用hasOwnProperty()
。
3. 使用安全的深度合并库
如果需要使用深度合并,建议使用一些经过安全审计的深度合并库,例如 lodash.merge
或 merge-deep
。 这些库通常会对 __proto__
属性进行过滤,防止原型污染。
4. 使用静态代码分析工具
可以使用一些静态代码分析工具,例如 ESLint 或 SonarQube,来检测代码中是否存在潜在的原型污染漏洞。 这些工具可以根据预定义的规则,自动扫描代码,并报告可能存在问题的代码片段。
5. 保持第三方库更新
及时更新使用的第三方库,以修复已知的原型污染漏洞。 许多第三方库都会定期发布安全更新,修复已知的漏洞。
6. 使用内容安全策略 (Content Security Policy, CSP)
可以使用 CSP 来限制 JavaScript 代码的执行,从而减少原型污染的风险。 CSP 是一种安全策略,可以控制浏览器可以加载哪些资源,例如 JavaScript、CSS、图片等。
7. 输入验证和输出编码
对用户输入进行严格的验证,防止恶意输入。 对输出进行编码,防止跨站脚本攻击 (Cross-Site Scripting, XSS),XSS攻击也可能间接导致原型污染。
8. 定期进行安全审计
定期对应用程序进行安全审计,检查是否存在潜在的原型污染漏洞。 可以使用一些专业的安全审计工具,或者请专业的安全专家进行审计。
五、一些额外的 Tips
- 养成良好的编码习惯: 编写代码时要时刻注意安全性,避免使用不安全的 API 和操作。
- 了解 JavaScript 原型链: 深入了解 JavaScript 原型链的机制,可以更好地理解原型污染的原理,从而更好地预防它。
- 关注安全社区: 关注安全社区的动态,及时了解最新的原型污染漏洞和防御方法。
六、总结
原型污染是一种非常隐蔽和危险的漏洞,它可以导致各种各样的安全问题。 为了防止原型污染,我们需要从多个方面入手,包括避免不安全的属性赋值、过滤敏感属性、使用安全的深度合并库、使用静态代码分析工具、保持第三方库更新、使用内容安全策略、输入验证和输出编码,以及定期进行安全审计。
希望通过今天的讲解,大家能够对原型污染有更深入的了解,并且能够在实际开发中有效地预防它。
好了,今天的讲座就到这里,谢谢大家! 散会!