
Prova de conceito demonstrando injeção de CRLF e contrabando de requisições HTTP no Axios, encadeando poluição de protótipo para alcançar SSRF e acessar serviços internos como IMDS.
Vulnerabilidade de injeção CRLF causada pelo fato de AxiosHeaders.set() do cliente HTTP Axios (>=1.0.0 <1.15.0, <0.31.0) não validar \r\n dentro dos valores de cabeçalho.
Se um atacante combinar isso com uma Poluição de Protótipo separada (lodash, qs, etc.) ou injetar CRLF diretamente nos cabeçalhos, é possível realizar SSRF para servidores internos arbitrários através de camadas intermediárias, como um proxy aberto nginx.
| Item | Conteúdo |
|---|
| CVSS | 9.9 (Crítico) |
| Versões afetadas | axios >=1.0.0 <1.15.0, axios <0.31.0 |
| Versões corrigidas | 1.15.0, 0.31.0 |
| CWE | CWE-93 Neutralização Incorreta de Sequências CRLF |
lib/core/AxiosHeaders.js AxiosHeaders.set()
└─ normalizeValue()
└─ Remove apenas CRLF final com /[\r\n]+$/
↑ \r\n no meio do valor passa direto
Como resultado, inserir \r\n\r\nGET /admin HTTP/1.1\r\n... no valor de um cabeçalho faz com que uma segunda requisição HTTP seja incluída diretamente no fluxo TCP.
Todos os objetos em JavaScript herdam de Object.prototype. A Poluição de Protótipo é um ataque que contamina esse protótipo compartilhado, afetando todos os objetos criados posteriormente.
Object.prototype.isAdmin = true;
const user = {};
user.isAdmin; // true ← existe mesmo sem ter sido declarado
Ocorre quando uma função de merge recursiva vulnerável (lodash < 4.17.21, qs, etc.) não trata a chave "__proto__" de forma especial.
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__"] → retorna Object.prototype
// → vulnerableMerge(Object.prototype, source["__proto__"])
// → injeta propriedade diretamente em Object.prototype
} else {
target[key] = source[key];
}
}
}
// JSON do atacante: __proto__ é analisado como propriedade própria
const payload = JSON.parse('{"__proto__":{"headers":{"X-Smuggle":"evil\\r\\n..."}}}');
vulnerableMerge({}, payload);
({}).headers; // { 'X-Smuggle': 'evil\r\n...' } ← poluição bem-sucedida
Object.prototype.headers = { 'X-Smuggle': 'evil\r\n...' }
↓
código do app: const opts = {};
opts.headers → cadeia de protótipos → retorna objeto poluído
↓
axios.get(url, { headers: opts.headers, adapter: rawSocketAdapter })
↓
AxiosHeaders.set('X-Smuggle', 'evil\r\n...') ← sem validação de CRLF
↓
requisição contrabandeada incluída no fluxo TCP
Poluir Object.prototype.headers também afeta os objetos de schema internos do axios.
Ao chamar assertOptions(config, schema)
schema['headers'] → cadeia de protótipos → retorna objeto poluído
validator(value) → chama objeto como função → TypeError
Em um ataque real, o escopo da poluição deve ser controlado com precisão, pois uma poluição ampla pode derrubar o próprio aplicativo antes do ataque pretendido (efeito colateral de DoS).
[1] Injeção direta de cabeçalho ou Poluição de Protótipo (via biblioteca de merge vulnerável)
↓
[2] axios serializa o valor com CRLF como cabeçalho sem validação
↓
[3] grava no fluxo TCP com net.Socket bruto
(o módulo http padrão do Node.js bloqueia em tempo de execução → adapter customizado necessário)
↓
[4] nginx (proxy_pass http://$http_host) analisa como 2 requisições separadas
↓
[5] roteia para outro upstream com base no cabeçalho Host da requisição contrabandeada (SSRF)
↓
[6] acesso a servidor interno (IMDS, etc.) → roubo de credenciais
| Condição | Conteúdo |
|---|---|
| Bypass do módulo http do Node.js | usar adapter customizado baseado em net.Socket |
| Proxy aberto nginx | configuração proxy_pass http://$http_host |
ignore_invalid_headers on | permite cabeçalhos anormais |
Diretiva resolver | permite resolução dinâmica de hostname |
┌─────────────────────────────────────────────────────────┐
│ Máquina Host │
│ │
│ test-axios-adapter-*.js │
│ (net.Socket bruto → nginx:8080) │
│ │
│ browser → exploit.html (3003) │
│ → relay (3004) → socket bruto → nginx:8080 │
└────────────────────┬────────────────────────────────────┘
│ Ponte Docker (cve-net)
┌──────────┼──────────┬──────────────┐
▼ ▼ ▼ ▼
backend nginx imds (futuro)
:3001 :8080 :80
:3003 (mock 169.254.169.254)
:3004
| Porta | Serviço | Função |
|---|---|---|
| 3001 | backend | Recebe requisições HTTP / registra cabeçalhos |
| 3003 | backend | Servidor estático do exploit.html |
| 3004 | backend | relay — converte POST do browser → net.Socket |
| 8080 | nginx | proxy aberto (proxy_pass http://$http_host) |
| 80 (interno) | imds | servidor mock AWS IMDSv2 |
# Construir e iniciar todos os contêineres
docker compose up --build
# Teste Node.js (executar no host)
npm install
# 1. Backend direto — injeção de CRLF gera 2 requisições
node poc/test-axios-adapter-backend.js
# 2. Via nginx — requisição contrabandeada roteada para backend:3001
node poc/test-axios-adapter-nginx.js
# 3. Poluição de Protótipo → Injeção CRLF → cadeia SSRF
node poc/test-prototype-pollution.js
# 4. Browser — http://localhost:3003
# Selecionar alvo: backend direto / via nginx / nginx → IMDS (SSRF)
1. Selecionar nginx → IMDS e executar o Adapter Customizado
2. Requisição contrabandeada: GET /latest/meta-data/iam/security-credentials/my-ec2-role
Host: imds
3. nginx roteia para host=imds → encaminha para o servidor mock IMDSv2
4. Log do contêiner imds: [!!!] Roubo de credenciais bem-sucedido!
.
├── docker-compose.yml
├── Dockerfile.backend # contêiner backend + relay
├── Dockerfile.imds # contêiner mock IMDSv2
├── package.json # [email protected] (versão vulnerável fixada)
└── poc/
├── backend-server.js # servidor HTTP (3001), relay (3004), servidor estático (3003)
├── mock-imds.js # mock AWS IMDSv2 (PUT /token, GET /credentials)
├── nginx-container.conf # configuração do proxy aberto
├── exploit.html # PoC no browser (XHR vs Adapter Customizado)
├── test-axios-adapter-backend.js # socket bruto → backend direto
├── test-axios-adapter-nginx.js # socket bruto → nginx → backend/imds
├── test-axios-no-adapter.js # axios padrão (para confirmar bloqueio do Node.js)
└── test-prototype-pollution.js # cadeia Poluição de Protótipo → Injeção CRLF
resolver 127.0.0.11 valid=30s; # DNS interno do Docker
location / {
proxy_pass http://$http_host; # roteamento dinâmico baseado no cabeçalho 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 fixo para requisições normais
}
Como $http_host (incluindo a porta) é usado como upstream, o cabeçalho Host da requisição contrabandeada se torna o destino do roteamento.
npm install axios@^1.15.0
O patch (1.15.0) introduz assertValidHeaderValue(), que rejeita imediatamente valores contendo CR/LF.
Defesas adicionais:
proxy_pass http://$http_host no nginx → usar upstream fixoHttpTokens: required)