原型污染是怎么发生的?怎样防护?

先理解原理和示例,再练习完整口述与追问。

危险来自把外部键写进对象,尤其是递归合并时读到 __proto__ 或 constructor.prototype,从而修改 Object.prototype。JSON.parse 本身用的是定义自有属性,解析出 __proto__ 键不会立刻污染全局。防护是拒绝这几个键、用 Map 或 Object.create(null) 做字典,并且不递归合并不可信对象。

obj['__proto__'] 走的是 Object.prototype 上的访问器:读取得到原型,赋值会改这个对象的原型。JSON.parse('{"__proto__":{"admin":true}}') 使用定义自有属性的方式,结果是一个名为 __proto__ 的普通键,此时 ({}).admin 仍是 undefined。

污染发生在随后的不安全合并。递归合并看到对象就继续往下写,target['__proto__'] 读出来的是 Object.prototype,于是下一层把 admin 写到了 Object.prototype 上,所有普通对象都带上了这个属性。constructor.prototype 是另一条同样的路径。for...in 还会走到继承属性,合并函数如果信任了来源对象的全部键,就会把这些键继续传播。

业务字典不需要原型方法时,用 Object.create(null) 或 Map。合并时跳过 __proto__、constructor、prototype,并且只用 Object.hasOwn 读取自有键。权限判断不要写成 if (user.admin) 这种可被原型字段冒充的形式,应读明确的自有字段。依赖库的深合并要锁定版本,补丁说明里出现 prototype pollution 就要升级。

js
function merge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
    const value = source[key]
    if (value && typeof value === 'object') {
      if (!Object.hasOwn(target, key) || typeof target[key] !== 'object') target[key] = {}
      merge(target[key], value)
    } else {
      target[key] = value
    }
  }
  return target
}

如果不跳过 __proto__,merge({}, JSON.parse('{"__proto__":{"admin":true}}')) 之后,一个新建对象也会读到 admin === true。跳过这些键之后,字段留在解析结果自己的自有属性上,不会写进 Object.prototype。

直接 target['__proto__'] = payload 改的是 target 自己的原型,不一定污染所有对象;面试时要把“改一个对象的原型”和“递归合并写进 Object.prototype”分开。hasOwnProperty 本身也可能被污染或覆盖,判断自有属性时用 Object.hasOwn,旧环境再用 Object.prototype.hasOwnProperty.call。

JSON.parse 把 proto 定义成自有属性,解析完不会立刻污染。危险在递归合并:读取 target 的 proto 会得到 Object.prototype,接着把子字段写进去,所有对象都多出这个属性。constructor.prototype 是另一条路径。我会在合并时丢弃 proto、constructor、prototype,字典用 Map 或 Object.create(null),权限字段只认自有属性。