什么是 JavaScript 原型污染?前端如何防范?
简化版
原型污染是攻击者通过 __proto__、constructor.prototype 等特殊路径修改对象原型,影响后续所有继承该原型的对象。前端常见于深合并、解析 query、按路径赋值等工具函数,防护要过滤危险 key、使用安全合并、创建无原型对象并避免信任继承属性。
详细版
危险示例:
function setByPath(obj, path, value) {
const keys = path.split('.')
let cur = obj
for (let i = 0; i < keys.length - 1; i += 1) {
cur = cur[keys[i]] ||= {}
}
cur[keys[keys.length - 1]] = value
}
setByPath({}, '__proto__.isAdmin', true)
console.log({}.isAdmin) // true
防护重点是禁止写入 __proto__、prototype、constructor 等危险键;读取配置时使用 Object.hasOwn;深拷贝和 merge 使用成熟库并及时升级;对 JSON、URL 参数、表单字段等用户可控路径做白名单校验。
完整版教学
一、为什么原型会被污染
JavaScript 对象通过原型链查找属性。普通对象找不到自有属性时,会继续到 Object.prototype 上查。如果攻击者能把恶意属性写到公共原型,很多看似干净的新对象都会“继承”这份污染。
user = {}
-> user.__proto__ === Object.prototype
-> Object.prototype.isAdmin = true
-> user.isAdmin 读取到 true
易错点:污染的可怕之处不在某一个对象被改了,而是后续大量对象的属性查找结果都被影响。
二、典型入口:深合并和路径赋值
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] ||= {}
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
return target
}
merge({}, JSON.parse('{"__proto__":{"polluted":true}}'))
console.log({}.polluted) // 可能变成 true
很多漏洞不是直接写 Object.prototype,而是工具函数递归处理用户输入时误入危险路径。query 解析器、表单生成器、配置合并器、国际化路径设置都可能成为入口。
三、污染后如何变成真实攻击
原型污染本身只是改变属性查找,真正危害来自后续代码把这些属性当成安全决策或 DOM 参数。
const options = {}
if (options.isAdmin) {
showAdminPanel()
}
假设 Object.prototype.isAdmin = true,上面逻辑就会误判。客户端还可能出现 DOM XSS gadget,例如某组件读取 options.innerHTML 并写入 DOM。污染 + gadget 组合,才会形成完整攻击链。
四、过滤危险 key
const BLOCKED_KEYS = new Set(['__proto__', 'prototype', 'constructor'])
function safeSet(target, key, value) {
if (BLOCKED_KEYS.has(key)) return
target[key] = value
}
过滤要覆盖每一级路径,而不只是第一层。constructor.prototype.x 和 a.__proto__.x 都可能危险。更严格的业务应采用白名单字段,只允许已知配置项进入合并逻辑。
五、使用无原型对象和自有属性检查
const dict = Object.create(null)
dict.safe = true
Object.hasOwn(dict, 'safe') // true
Object.create(null) 创建的对象没有 Object.prototype,适合作字典表。读取安全敏感字段时,不要直接用 if (obj.isAdmin),应检查自有属性并设置明确默认值。
| 写法 | 风险 |
|---|---|
if (config.enabled) | 可能读取继承属性 |
Object.hasOwn(config, 'enabled') | 只看自有属性 |
for...in | 会遍历可枚举继承属性 |
Object.keys() | 只返回自有可枚举属性 |
六、依赖库和运行时防护
历史上不少深拷贝、merge、querystring 库都出现过原型污染漏洞。前端项目应固定锁文件、及时升级依赖,并对安全公告敏感。对于不可信 JSON,不要随意深合并到全局配置。
某些运行时支持禁用或限制 __proto__ 行为,但前端不能假设所有浏览器都有同样能力。工程上更稳的是:用户输入白名单、危险 key 黑名单、安全工具函数和依赖审计一起做。
七、常见误区与追问
- 误区:原型污染只影响 Node.js。 客户端 JavaScript 同样有原型链和 DOM gadget 风险。
- 误区:只过滤第一层
__proto__就够了。 嵌套路径和constructor.prototype也要处理。 - 误区:污染后一定立刻 RCE。 前端更多是权限绕过、DOM XSS、逻辑污染,危害取决于 gadget。
- 追问:为什么
for...in危险? 它会遍历可枚举继承属性,可能把污染属性当作正常输入。 - 追问:如何判断属性是自己的? 使用
Object.hasOwn(obj, key)或安全的hasOwnProperty.call。 - 追问:对象字典为什么可用
Object.create(null)? 它没有原型链,减少继承属性干扰。
八、加强记忆
原型污染可以按“入口、污染、利用、防护”串起来:入口是深合并、路径赋值和 query 解析;污染是写入 __proto__ 或 constructor.prototype;利用依赖后续代码读取继承属性或危险 DOM 参数;防护靠危险 key 过滤、字段白名单、自有属性检查、无原型字典和依赖升级。它考的是你对 JS 原型链和安全边界的共同理解。