属性白名单校验的核心目标是仅保留预设合法字段,忽略其余所有字段,且必须前置执行以阻断原型链污染;白名单须硬编码、不可动态修改,并需额外显式拦截__proto__等危险键;推荐直接遍历白名单结合hasOwnProperty赋值,避免先全量提取再过滤。

属性白名单校验在浅拷贝阶段的核心目标很明确:只保留预设的合法字段名,其余一概忽略,不复制、不报错、不静默覆盖。它不解决深拷贝中的嵌套污染问题,但能从源头切断原型链污染(如 __proto__、constructor)和业务无关字段(如前端传来的 _internal_flag)进入目标对象。
白名单校验应放在拷贝逻辑最前端
不是先合并再过滤,而是“只取白名单里的键”。哪怕源对象有 100 个字段,只要不在白名单中,就完全跳过。
- 对每个待拷贝的 key,先用
whitelist.includes(key)或whitelist.has(key)判断是否允许 - 避免使用
Object.keys(obj).filter(...).forEach(...)这类先全量提取再筛选的方式——效率低,且可能触发 getter 副作用 - 推荐直接遍历白名单数组:
whitelist.forEach(key => { if (source.hasOwnProperty(key)) target[key] = source[key]; })
白名单本身要定义清晰、不可动态修改
白名单不应来自配置文件或用户输入,而应是硬编码的常量或由类型系统(如 TypeScript interface)约束的固定字段集。
- 例如:
const USER_COPY_WHITELIST = ['id', 'username', 'email', 'age'] as const; - 配合 TypeScript 的
keyof typeof USER_COPY_WHITELIST可确保后续赋值不会拼错字段名 - 禁止用字符串拼接、变量拼装等方式构造白名单,否则可能绕过校验
特别注意特殊键名的显式排除
即使白名单里没写,也要额外检查是否含危险键——因为攻击者可能通过路径拼接(如 a.__proto__.b)绕过简单 key 匹配。
- 在取 key 之前,先做一次字符串级拦截:
if (key === '__proto__' || key === 'constructor' || key === 'prototype') return; - 若支持点号路径(如
user.profile.__proto__.admin),需对整个 path 字符串做子串检测:path.includes('__proto__'),或拆分为段后逐段比对 - 这个检查必须在任何属性访问前执行,不能依赖白名单覆盖——白名单只是业务字段控制,不是安全边界
与 Object.assign 配合使用更轻量可靠
Object.assign 本身就是浅拷贝,且不递归、不处理 __proto__ 注入(现代引擎已限制),天然比自研合并函数更安全。
- 可封装一个白名单版 assign:
function assignWhitelist(target, source, whitelist) { whitelist.forEach(k => { if (source.hasOwnProperty(k)) target[k] = source[k]; }); return target; } - 不建议在
Object.assign后再做“剔除”操作,因为危险属性可能已在原型上生效(如{__proto__: {x:1}}已污染) - 永远不要把用户输入直接作为
source传给未加白名单的Object.assign










