概念
原型污染安全漏洞(Prototype Pollution) 是前端 / Node.js 生态里非常重要、也非常容易被忽略的一类漏洞。该漏洞最终可导致权限绕过,是前端安全中的高危问题。
什么是原型污染?
攻击者通过可控输入,修改 Object.prototype 或其他内建原型,从而影响所有对象的行为。
JS 原型链
javascript
obj → Object.prototype → null
一旦污染了 Object.prototype
ini
Object.prototype.isAdmin = true;
({}).isAdmin === true; // 💣
模拟攻击
下面我们通过一段代码来分析一下什么是原型污染。
js
function deepMerge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) {
target[key] = {};
}
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// 模拟业务代码
// 方案1:监听 message 事件
const config = {};
window.addEventListener('message', e => {
deepMerge(config, e.data);
});
// 方案2:使用 qs 解析 query
import qs from 'qs';
const searchQuery = qs.parse(location.search);
const routeQuery = {
type: 'low',
tab: 1,
};
deepMerge(routeQuery, searchQuery);
// 业务逻辑
const userInfo = {};
if (userInfo.isAdmin) {
console.log('你是管理员');
}
// 我想成为管理员,请输入攻击荷载:
这段代码存在典型的原型污染(Prototype Pollution)安全漏洞,而且两个"方案"都能成为攻击入口。下面我按「问题 → 利用方式 → 影响 → 修复建议」来拆解。
一、核心问题在哪?
不安全的 deepMerge
js
function deepMerge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) {
target[key] = {};
}
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
问题点:
- 使用
for...in(会遍历原型链) - 未过滤
proto/constructor/prototype - 未判断是否为「纯对象」
- 直接递归写入
target[key]
👉 攻击者可以修改 Object.prototype
二、两个业务场景为什么都危险?
方案1:
js
window.addEventListener('message', e => {
deepMerge(config, e.data);
});
攻击 payload
js
window.postMessage({proto: {isAdmin: true
}
}, '*');
结果
js
const userInfo = {};
userInfo.isAdmin === true; // 💣
🎯 攻击者无需控制 userInfo,只要污染原型即可
方案2:
js
qs.parse(location.search)
const searchQuery = qs.parse(location.search);
deepMerge(routeQuery, searchQuery);
恶意 URL
js
https://example.com/?_proto_[isAdmin]=true
或(更隐蔽)
js
?constructor[prototype][isAdmin]=true
结果同样是:
js
({}).isAdmin === true;
三、最终危害点(业务逻辑被绕过)
js
const userInfo = {};
if (userInfo.isAdmin) {
console.log('你是管理员');
}
🚨 这是一个权限绕过漏洞 在真实系统中,可能导致:
- 后台权限获取
- 功能越权
- 安全校验全部失效
四、为什么这是"高危漏洞"?
| 特性 | 说明 |
|---|---|
| 利用门槛 | 低(URL / postMessage 即可) |
| 影响范围 | 全局(污染 Object.prototype) |
| 隐蔽性 | 高(调试很难发现) |
| 常见度 | 非常高(deepMerge / lodash.merge) |
👉 已多次出现在真实 CVE (Common Vulnerabilities and Exposures) 中。
五、正确的修复方式 ✅
✅ 1. 严格过滤危险 key(必须)
js
const blockedKeys = ['__proto__', 'prototype', 'constructor'];
function safeDeepMerge(target, source) {
for (const key of Object.keys(source)) {
if (blockedKeys.includes(key)) continue;
if (
typeof source[key] === 'object' &&
source[key] !== null &&
!Array.isArray(source[key])
) {
if (!target[key]) target[key] = {};
safeDeepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
✅ 2. postMessage 必须校验来源
js
window.addEventListener('message', e => {
if (e.origin !== 'https://trusted.com') return;
safeDeepMerge(config, e.data);
});
✅ 3. qs 使用安全模式
js
qs.parse(location.search, {
allowPrototypes: false
});
⚠️ 默认 qs 是危险的(旧版本尤其)
✅ 4. 使用成熟库
如 URLSearchParams
js
const params = Object.fromEntries(new URLSearchParams(location.search));
✅ 5. 防御性编程(推荐)
js
const userInfo = Object.create(null);
或
js
if (Object.prototype.hasOwnProperty.call(userInfo, 'isAdmin')) {
...
}
为什么它是"安全漏洞"而不是 Bug?
因为它会导致:
| 后果 | 示例 |
|---|---|
| 权限绕过 | if (user.isAdmin) |
| 逻辑篡改 | if (config.debug) |
| RCE(Node) | 与 eval / child_process 结合 |
| XSS | 模板渲染、配置拼接 |
| DoS | JSON stringify / 递归崩溃 |
👉 影响是"全局的、隐式的、持久的"
真实世界中的 CVE (Common Vulnerabilities and Exposures)
| 库 | CVE |
|---|---|
| lodash.merge | CVE-2019-10744 |
| jQuery.extend | CVE-2019-11358 |
| minimist | CVE-2020-7598 |
| yargs | CVE-2020-7608 |
| qs | 多次 |
📌 这不是"理论漏洞",而是"被打烂的漏洞类型"
如何判断你的代码是否有原型污染?
🔍 自查 checklist
- 是否把 用户输入 merge 到对象?
- 是否使用
for...in? - 是否递归拷贝对象?
- 是否信任
qs.parse/ JSON? - 是否存在
obj[key] = value,且key来自外部?
如果有 2 条以上,高风险。