实战剖析原型链污染漏洞(CVE-2019-10744):从攻击链到防御策略
1. 从一次“诡异”的权限绕过说起几年前我在排查一个内部管理系统的安全问题时遇到了一件怪事。系统日志显示一个普通用户账号竟然成功访问了只有管理员才能进入的“后台用户管理”页面。我们反复检查了数据库里的用户角色字段确认这个账号的role字段明明白白写着user而不是admin。前端、后端的权限校验逻辑看起来也毫无破绽。问题到底出在哪经过一番“抽丝剥茧”我们把目光锁定在了一个处理用户配置更新的API接口上。这个接口接收前端传来的JSON数据然后使用一个非常流行的工具库lodash中的_.merge函数将用户提交的配置与默认配置进行深度合并。攻击者提交的请求体看起来人畜无害但里面藏着一个“魔法”属性__proto__。正是这个不起眼的属性引发了一场全局性的“污染”最终导致了权限校验的全面失效。这个漏洞就是后来被正式编号为CVE-2019-10744的原型链污染漏洞。你可能觉得“原型链污染”这个词听起来很学术、很复杂。别担心我们可以把它想象成一次“自来水厂投毒”事件。在JavaScript的世界里Object.prototype就像是整个城市的“供水总管道”几乎所有对象可以理解为每家每户的水龙头都间接地从这里取水继承属性。攻击者通过一个特定的漏洞比如有问题的_.merge函数成功地向Object.prototype这个“总管道”里注入了一种名为isAdmin: true的“毒素”。从此以后任何一个新建的普通对象只要它自己没有isAdmin这个属性当它去查找时就会顺着“水管”原型链找到被污染的总管道从而“喝到”这口毒水认为自己拥有管理员权限。这篇文章我就带你一起像侦探一样完整地复盘这次攻击的链条。我们会从最基础的JavaScript原型链讲起亲手复现攻击过程看看攻击者是如何一步步“污染”整个环境的。更重要的是我会分享从代码编写、依赖管理到线上运维的一整套“解毒”和“防护”方案。无论你是前端还是Node.js后端开发者理解这个漏洞都能让你在编写更安全的代码时多一份警惕和底气。2. 漏洞原理JavaScript原型链的“阿喀琉斯之踵”要理解污染是如何发生的我们得先回到JavaScript这门语言一个非常核心但又常常被忽略的特性原型继承。这不像经典的类继承Java/C那种而是一种更灵活的“委托”机制。2.1 原型链对象的“家族族谱”在JavaScript中几乎所有的对象都有一个隐藏的链接指向另一个对象这个链接就是__proto__属性在ES6标准中更推荐使用Object.getPrototypeOf()来获取。这个被指向的对象就是它的“原型”。当你试图访问一个对象的某个属性比如obj.myProperty时JavaScript引擎会执行一个非常简单的“查找算法”先在对象自身查找看看obj自己有没有名叫myProperty的属性。如果没找到就去它的原型对象上找也就是去obj.__proto__指向的对象上找。如果还没找到就继续往原型的原型上找这就形成了一条链我们称之为原型链。直到找到属性或者到达链条的尽头null。我们可以用一段简单的代码来感受一下// 创建一个普通的空对象 let myObject {}; console.log(myObject.toString); // 输出[Function: toString] // myObject 自己有 toString 方法吗 console.log(myObject.hasOwnProperty(toString)); // 输出false // 那这个方法从哪来的来自它的原型 console.log(myObject.__proto__.hasOwnProperty(toString)); // 输出true console.log(myObject.__proto__ Object.prototype); // 输出true你看myObject本身空空如也但它却能调用toString方法。这个方法就是从它的原型也就是Object.prototype这个“老祖宗”那里继承来的。Object.prototype是所有通过字面量{}或new Object()创建的对象的原型终点它上面定义了许多像toString、valueOf、hasOwnProperty这样的基础方法。2.2 污染是如何发生的篡改“族谱”的根基理解了原型链的查找机制攻击的思路就清晰了如果我能修改Object.prototype本身那么所有继承自它的对象行为都会发生改变。正常情况下我们无法直接通过一个普通对象的赋值来修改Object.prototype。比如let obj {}; obj.__proto__.isAdmin true; // 在严格模式下直接设置__proto__可能被禁止或无效但是漏洞就出现在一些“深度合并”或“对象拷贝”的函数中。以存在漏洞的lodash库4.17.11及之前版本中的_.merge函数为例它的本意是递归地将源对象的所有属性合并到目标对象中。其内部简化逻辑类似这样function unsafeMerge(target, source) { for (let key in source) { if (typeof source[key] object source[key] ! null) { // 如果是对象则递归合并 target[key] target[key] || {}; unsafeMerge(target[key], source[key]); } else { // 如果是基本类型直接赋值 target[key] source[key]; } } }问题的关键在于它在遍历source对象的属性时没有对特殊的属性名__proto__进行过滤当攻击者构造一个像{__proto__: {isAdmin: true}}这样的恶意对象时函数会执行这样的操作key是__proto__。source[key]的值是{isAdmin: true}它是一个对象。函数进入递归合并分支target[key] target[key] || {};。这里的target可能是任何一个空对象{}。致命的一步target[__proto__]这个赋值操作在当时的JavaScript引擎和lodash的实现上下文下并没有在target对象上创建一个名为__proto__的普通属性而是直接修改了target对象的原型这就相当于把target的原型指向了一个新对象{isAdmin: true}。由于target本身可能就是一个普通对象其原型就是Object.prototype。在某些情况下这个操作实际上就污染了Object.prototype给它加上了isAdmin: true这个属性。注意现代JavaScript引擎V8等已经对直接通过__proto__赋值修改对象原型的行为做了很多限制但在特定的库函数实现和旧版本环境中这种污染路径依然可能被触发。CVE-2019-10744正是利用了lodash库中baseMerge等函数未过滤__proto__和constructor属性的缺陷。一旦污染成功后果是全局性的// 假设 Object.prototype 已被污染 console.log(({}).isAdmin); // 输出true console.log([].isAdmin); // 输出true console.log(Object.prototype.isAdmin); // 输出true // 甚至新创建的函数也“中毒”了 function foo() {} console.log(foo.isAdmin); // 输出true整个应用的根基被动摇了所有对象的默认行为都变得不可预测。3. 实战攻击链拆解从污染到掌控明白了原理我们来看看攻击者是如何利用这个漏洞在真实场景中“兴风作浪”的。我会用两个最典型的场景带你完整复现一遍。3.1 场景一权限绕过——让普通用户“变身”管理员这是最直接、危害也最大的利用方式。假设我们有一个简单的Node.js Express用户API。第一步存在漏洞的代码const express require(express); const _ require(lodash); // 版本为有漏洞的4.17.11 const app express(); app.use(express.json()); let userConfig { theme: light, notifications: true }; // 一个更新用户配置的接口 app.post(/api/update-config, (req, res) { // 危险操作使用有漏洞的_.merge深度合并用户输入 _.merge(userConfig, req.body); res.json({ message: Config updated, config: userConfig }); }); // 一个需要管理员权限的接口 app.get(/api/admin/users, (req, res) { // 假设从session或token中解析出了用户对象req.user // 权限校验检查用户角色 if (req.user.role ! admin) { // 这里使用了 ! 进行判断 return res.status(403).send(Forbidden); } res.json({ secret: List of all users... }); });注意看权限校验这行代码if (req.user.role ! admin)。如果req.user对象本身没有role属性JavaScript就会去它的原型链上找。第二步发动攻击攻击者并不直接去攻击/api/admin/users接口而是先调用那个存在漏洞的配置更新接口。他发送一个POST请求到/api/update-config请求体如下{ theme: dark, __proto__: { role: admin } }有漏洞的_.merge函数在处理这个请求体时会执行_.merge(userConfig, req.body)。如前所述它没有过滤__proto__属性导致Object.prototype被添加了一个role属性其值为admin。第三步污染生效权限绕过此时Object.prototype.role已经等于admin。当同一个用户或者任何其他用户因为污染是全局的后续访问/api/admin/users时中间件解析出req.user对象假设是{userId: 123, name: attacker}。执行权限校验req.user.role ! admin。JavaScript引擎发现req.user对象自身没有role属性。于是它沿着原型链查找找到了Object.prototype.role其值为admin。判断条件admin ! admin结果为false于是权限校验通过攻击者成功以普通用户身份获取了管理员数据。这个过程就像给所有进入系统的人都偷偷发了一张伪造的“管理员通行证”。只要他没被搜身自身没有role属性就能凭这张通用的假证畅通无阻。3.2 场景二XSS攻击升级——污染模板引擎的“安全开关”原型链污染的可怕之处在于它不仅能影响业务逻辑还能影响其他库的安全机制。我们来看一个结合模板引擎的案例。很多模板引擎比如Handlebars为了提高性能或灵活性会将一些配置选项存储在自身的某个对象中。如果这个对象继承自Object.prototype那么它就有可能被污染。攻击目标关闭HTML转义Handlebars默认会对变量输出进行HTML转义防止XSS。例如{{content}}会转义而{{{content}}}三个大括号不会。但引擎内部有一个escapeExpression函数来控制转义行为。攻击步骤寻找污染点应用某处同样使用了有漏洞的_.merge或类似函数处理用户可控数据。构造污染载荷攻击者提交数据污染Object.prototype上的某个Handlebars内部使用的配置属性名。经过研究或利用公开的PoC攻击者发现可以污染escapeExpression属性。// 攻击者通过漏洞接口发送的恶意数据 const maliciousPayload { __proto__: { escapeExpression: function(str) { // 覆盖默认的转义函数直接返回原始字符串不做任何转义 return str; } } }; // 假设应用某处执行了可能是配置合并、数据缓存等 _.merge(someInternalConfig, maliciousPayload);污染生效Object.prototype.escapeExpression被替换成了一个“不做转义”的函数。触发XSS当Handlebars编译模板并需要转义变量时它会调用escapeExpression函数。由于原型链查找它找到了被污染的函数于是该变量不再被转义。const Handlebars require(handlebars); // 此时Object.prototype.escapeExpression 已被污染 const templateSource div{{{content}}}/div; // 注意这里原本用了三个大括号但污染后两个大括号的也会受影响 const template Handlebars.compile(templateSource); // 用户输入恶意脚本 const userInput img srcx onerroralert(XSS via Prototype Pollution); const htmlOutput template({ content: userInput }); console.log(htmlOutput); // 输出divimg srcx onerroralert(XSS via Prototype Pollution)/div // 原本应该被转义的尖括号等字符现在原样输出XSS攻击成功通过这种方式攻击者无需直接找到模板变量的注入点而是通过污染底层函数“一键关闭”了整个应用甚至整个服务器的HTML转义安全防护极大地扩大了XSS的攻击面。4. 构建多维防御体系从代码到运维的全面布防知道了攻击者怎么来我们就能有针对性地筑起防线。防御原型链污染绝不能只靠一招需要建立一个从内到外的立体防御体系。4.1 代码层加固编写“免疫”的合并函数这是最根本的防御确保你的代码在处理不可信对象时能够主动拒绝__proto__、constructor、prototype等敏感属性。方案一使用安全的库首先立即升级你的lodash到4.17.12或更高版本。官方在这个版本中修复了_.merge、_.defaultsDeep等相关函数的漏洞。对于其他库也要保持关注安全公告并及时升级。方案二实现自己的安全合并函数如果因为某些原因必须自己实现深度合并下面是一个简单的安全版本示例function safeDeepMerge(target, source) { // 遍历源对象的所有自有属性不包括原型链上的 for (let key in source) { if (!source.hasOwnProperty(key)) { continue; // 跳过从原型链继承来的属性 } // 关键过滤拒绝 __proto__ 和 constructor 等特殊属性 if (key __proto__ || key constructor || key prototype) { continue; } const sourceVal source[key]; const targetVal target[key]; // 递归合并对象和数组 if (sourceVal typeof sourceVal object !Array.isArray(sourceVal)) { if (targetVal typeof targetVal object) { safeDeepMerge(targetVal, sourceVal); } else { // 使用Object.create(null)创建无原型的对象更安全 target[key] safeDeepMerge(Object.create(null), sourceVal); } } else { // 赋值基本类型或数组注意数组是浅拷贝根据需求可调整 target[key] sourceVal; } } return target; } // 使用示例 const config { app: { name: MyApp } }; const userInput JSON.parse({__proto__:{polluted: true}, app: {theme: dark}}); const result safeDeepMerge(config, userInput); console.log(result); // { app: { name: MyApp, theme: dark } } console.log(({}).polluted); // undefined 污染失败这个函数有几个关键点使用hasOwnProperty确保只处理对象自身的属性。显式过滤__proto__、constructor、prototype。在递归创建新对象时使用Object.create(null)创建没有原型的“纯净字典”彻底断绝被污染的可能。对于数组这里采用了直接赋值浅拷贝在实际应用中你需要根据业务逻辑决定是替换、合并还是其他操作。4.2 框架层/应用层防护输入净化与请求拦截在代码的具体函数之外我们可以在请求入口处建立一个“消毒站”。方案一Express中间件进行全局净化在Node.js的Express框架中可以编写一个中间件对所有传入的请求体req.body、查询参数req.query进行递归遍历删除其中的敏感属性。const protoPollutionSanitizer (req, res, next) { const sanitize (obj) { if (obj typeof obj object) { // 删除对象自身的 __proto__ 和 constructor 属性 delete obj.__proto__; delete obj.constructor; // 递归处理对象的子属性 Object.values(obj).forEach(value { if (value typeof value object) { sanitize(value); } }); } }; // 净化请求体、查询参数、路由参数 sanitize(req.body); sanitize(req.query); sanitize(req.params); next(); // 继续后续处理 }; // 在应用中使用 const app express(); app.use(express.json()); app.use(protoPollutionSanitizer); // 在所有路由之前使用方案二在反序列化环节进行防御如果数据来自JSON字符串可以在JSON.parse之后立即进行净化。或者更安全地使用一个能够识别并拒绝畸形__proto__的JSON解析器。function safeJsonParse(str) { const obj JSON.parse(str); const sanitized JSON.parse(str, (key, value) { if (key __proto__ || key constructor || key prototype) { return undefined; // 遇到敏感键直接丢弃该属性 } return value; }); return sanitized; }4.3 依赖与运维层守住供应链大门现代应用开发严重依赖开源库一个底层库的漏洞可能危及整个系统。方案一自动化依赖漏洞扫描将安全工具集成到你的开发流程中本地扫描使用npm audit或yarn audit命令定期检查项目依赖。CI/CD集成在持续集成流水线中加入安全扫描步骤。例如使用Snyk、GitHub Dependabot或GitLab Dependency Scanning。# 示例在CI脚本中使用Snyk CLI进行测试和监控 npm install -g snyk snyk auth # 认证 snyk test --severity-thresholdhigh # 测试并只报告高危漏洞 snyk monitor # 将项目状态上传到Snyk平台进行持续监控容器镜像扫描对构建出的Docker镜像进行漏洞扫描确保运行环境安全。方案二依赖锁定与最小化使用锁文件务必使用package-lock.json或yarn.lock确保所有环境安装的依赖版本一致避免意外升级到有漏洞的版本也避免意外安装了被污染的包。定期更新依赖有计划地、定期地更新项目依赖特别是那些直接处理用户输入、网络通信、序列化/反序列化的核心库。不要长期使用已停止维护的旧版本。方案三运行时防护与监控对象冻结对于重要的基础配置对象可以考虑使用Object.freeze()将其冻结防止其被修改包括原型。const defaultConfig Object.freeze({ appName: SecureApp, version: 1.0 }); // defaultConfig.__proto__ something; // 严格模式下会报错使用Proxy进行监控对于关键对象可以使用ES6的Proxy来监控对其属性的所有访问和设置操作一旦发现试图设置__proto__等属性就发出警报或阻止。完善的日志记录记录所有用户输入、特别是导致错误或异常行为的输入。当发生疑似原型链污染攻击时比如大量请求包含__proto__关键字日志能帮助你快速定位问题源头。防御原型链污染是一个需要贯穿软件开发生命周期SDLC的持续过程。从编写第一行代码时的安全意识到依赖库的谨慎选择再到生产环境的严密监控每一环都不可或缺。它提醒我们在JavaScript这个灵活强大的语言生态里安全不仅仅是处理显式的用户输入更要深入到语言特性本身防范那些基于其动态原型机制的“魔法”攻击。

相关新闻

从零推导到电路仿真:二阶压控电压源高通滤波器传递函数实战解析

从零推导到电路仿真:二阶压控电压源高通滤波器传递函数实战解析

1. 从“虚短虚断”开始:手把手推导传递函数 很多朋友一看到电路图里又是运放又是电容电阻,就觉得头大,公式更是像天书。别怕,今天咱们就从一个最经典的二阶压控电压源高通滤波器电路开始,我带着你,像解一道…

2026/7/5 13:55:56 阅读更多 →
FreeRTOS下Easylogger异步日志的缓冲区优化与性能调优

FreeRTOS下Easylogger异步日志的缓冲区优化与性能调优

1. 从日志丢失的“灵异事件”说起:为什么你的异步日志不灵了? 最近在调试一个基于FreeRTOS的嵌入式项目,用上了Easylogger的异步输出模式,本以为从此可以高枕无忧,让日志在后台默默输出,不打扰主任务的执行…

2026/7/4 11:03:12 阅读更多 →
ABAP 主调程序动态获取被调程序ALV数据列表的三种实现方式

ABAP 主调程序动态获取被调程序ALV数据列表的三种实现方式

1. 为什么需要动态获取另一个程序的ALV数据? 在SAP的日常开发中,经常会遇到一个挺有意思的场景:程序A(主调程序)需要获取程序B(被调程序)里已经展示在ALV报表上的数据,然后对这些数据…

2026/5/17 12:29:01 阅读更多 →

最新新闻

如何在5分钟内用Containerlab构建企业级网络实验室

如何在5分钟内用Containerlab构建企业级网络实验室

如何在5分钟内用Containerlab构建企业级网络实验室 【免费下载链接】containerlab container-based networking labs 项目地址: https://gitcode.com/gh_mirrors/co/containerlab 你是否曾经为了测试一个新的网络配置而不得不搭建复杂的物理环境?或者因为缺乏…

2026/7/6 18:26:06 阅读更多 →
Blockly 终极指南:15分钟从零搭建你的第一个可视化编程项目

Blockly 终极指南:15分钟从零搭建你的第一个可视化编程项目

Blockly 终极指南:15分钟从零搭建你的第一个可视化编程项目 【免费下载链接】blockly The web-based visual programming editor. 项目地址: https://gitcode.com/gh_mirrors/bl/blockly 你是否曾因复杂的代码语法而对编程望而却步?是否想为孩子或…

2026/7/6 18:26:06 阅读更多 →
MAX22000与TM4C1299KCZAD的高精度信号转换系统设计

MAX22000与TM4C1299KCZAD的高精度信号转换系统设计

1. MAX22000与TM4C1299KCZAD的硬件协同设计在工业测量和控制系统中,信号转换的精度和实时性直接决定了整个系统的性能表现。MAX22000作为一款高精度模拟前端(AFE)芯片,与TM4C1299KCZAD微控制器的组合,为解决复杂信号处理需求提供了理想的硬件…

2026/7/6 18:26:06 阅读更多 →
解密3步:零成本解锁Unity全版本专业功能

解密3步:零成本解锁Unity全版本专业功能

解密3步:零成本解锁Unity全版本专业功能 【免费下载链接】UniHacker 为Windows、MacOS、Linux和Docker修补所有版本的Unity3D和UnityHub 项目地址: https://gitcode.com/GitHub_Trending/un/UniHacker 你是否曾因Unity专业版的许可证费用而望而却步&#xff…

2026/7/6 18:22:04 阅读更多 →
Playwright CLI终极指南:从零开始掌握浏览器自动化测试

Playwright CLI终极指南:从零开始掌握浏览器自动化测试

Playwright CLI终极指南:从零开始掌握浏览器自动化测试 【免费下载链接】playwright-cli CLI for common Playwright actions. Record and generate Playwright code, inspect selectors and take screenshots. 项目地址: https://gitcode.com/GitHub_Trending/pl…

2026/7/6 18:20:02 阅读更多 →
M3u8Downloader_H未来路线图:即将推出的新功能与改进计划

M3u8Downloader_H未来路线图:即将推出的新功能与改进计划

M3u8Downloader_H未来路线图:即将推出的新功能与改进计划 【免费下载链接】M3u8Downloader_H m3u8下载器,功能强大,多线程,多任务,支持aes-128-cbc解密,自定义请求头,自定义插件 项目地址: https://gitcode.com/gh_mirrors/m3/M3u8Downloader_H M3u8Download…

2026/7/6 18:15:59 阅读更多 →

日新闻

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2与MySQL单元测试兼容性:5个关键SQL语句差异与规避方案1. 单元测试中的数据库兼容性挑战在Java开发领域,单元测试是保证代码质量的重要环节。当应用涉及数据库操作时,测试环境的搭建往往成为开发者的痛点。H2数据库因其轻量级、内存模式和快…

2026/7/6 0:01:17 阅读更多 →
Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘 【免费下载链接】rbtray A fork of RBTray from http://sourceforge.net/p/rbtray/code/. 项目地址: https://gitcode.com/gh_mirrors/rb/rbtray 你是否厌倦了Windows任务栏上密密麻麻的图标&…

2026/7/6 0:01:17 阅读更多 →
Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C 运行时库一键安装终极指南:告别DLL缺失烦恼 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾经遇到过这样的情况:下载了…

2026/7/6 0:05:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/6 8:11:50 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/6 8:11:52 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/6 6:52:56 阅读更多 →

月新闻