Category: Linux · Web · Lateral Movement
Level: Practitioner
There is a class of misconfiguration that appears everywhere in modern infrastructure and is consistently underestimated: trusting client-controlled HTTP headers for access control decisions.
The scenario is common. A public-facing server runs Nginx as a reverse proxy in front of an internal application. The application has an admin panel that should only be accessible from localhost. Someone — reasonably, it seems — decides to enforce this by checking the origin IP in the request. The problem is where they check it.
location /admin {
if ($http_x_forwarded_for != "127.0.0.1") {
return 403;
}
proxy_pass http://127.0.0.1:8080;
}
$http_x_forwarded_for is the value of the X-Forwarded-For header. That header is set by the client. Any external user can send:
GET /admin/diagnostics HTTP/1.1
X-Forwarded-For: 127.0.0.1
And the check passes. The admin panel is now accessible from anywhere on the internet.
The confusion comes from how X-Forwarded-For is supposed to work. In a properly configured stack, the header is set by a trusted upstream proxy — not the client. The internal application reads it to know the real client IP after going through load balancers.
But when Nginx is the first point of contact (the internet-facing layer), it is receiving headers from untrusted clients. Trusting those headers for security decisions is equivalent to asking someone for their ID and accepting whatever they write on a piece of paper.
At the Nginx level — use $remote_addr (the actual TCP connection IP) instead of the forwarded header:
location /admin {
allow 127.0.0.1;
deny all;
proxy_pass http://127.0.0.1:8080;
}
This cannot be forged. The allow/deny directives operate on the real network layer.
At the application level — never make authorization decisions based on X-Forwarded-For alone. If the application needs the real IP, configure Nginx to set a trusted header using $remote_addr:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
And strip any client-supplied version of those headers before forwarding:
proxy_set_header X-Forwarded-For "";
The reason this is worth writing about is not the misconfiguration itself — it is what came after in a real exercise. The accessible admin panel exposed system diagnostics. The diagnostics exposed log fragments. The log fragments exposed a temporary SSH key path. The key gave access to an internal network segment. The flag was on a host with no public route.
One bad header. Full internal network access.
The surface looked small. Two public ports, no obvious login form, no CMS. The weakness was invisible to a passive scan. It only showed up when you paid attention to how the proxy behaved under different inputs.
X-Forwarded-For or X-Real-IP for access control at the edge layer — they are client-controlledallow/deny, firewall rules), not header checksRelated: Case 01 — Linux Pivot via Reverse Proxy Misconfiguration
Categoria: Linux · Web · Movimentação Lateral
Nível: Praticante
Existe uma classe de misconfiguration que aparece em toda infraestrutura moderna e é consistentemente subestimada: confiar em headers HTTP controlados pelo cliente para decisões de controle de acesso.
O cenário é comum. Um servidor público roda Nginx como proxy reverso na frente de uma aplicação interna. A aplicação tem um painel de administração que deveria ser acessível apenas a partir do localhost. Alguém decide reforçar isso verificando o IP de origem da requisição. O problema está em onde verificam.
location /admin {
if ($http_x_forwarded_for != "127.0.0.1") {
return 403;
}
proxy_pass http://127.0.0.1:8080;
}
$http_x_forwarded_for é o valor do header X-Forwarded-For. Esse header é definido pelo cliente. Qualquer usuário externo pode enviar:
GET /admin/diagnostics HTTP/1.1
X-Forwarded-For: 127.0.0.1
E a verificação passa. O painel de administração agora é acessível de qualquer lugar na internet.
A confusão vem de como o X-Forwarded-For deveria funcionar. Em uma stack bem configurada, o header é definido por um proxy upstream confiável — não pelo cliente. A aplicação interna o lê para saber o IP real do cliente após passar por load balancers.
Mas quando o Nginx é o primeiro ponto de contato (a camada voltada para a internet), ele está recebendo headers de clientes não confiáveis. Confiar nesses headers para decisões de segurança é equivalente a pedir um documento a alguém e aceitar qualquer coisa que ele escrever num papel.
No nível do Nginx — use $remote_addr (o IP real da conexão TCP) em vez do header encaminhado:
location /admin {
allow 127.0.0.1;
deny all;
proxy_pass http://127.0.0.1:8080;
}
Isso não pode ser falsificado. As diretivas allow/deny operam na camada de rede real.
No nível da aplicação — nunca tome decisões de autorização baseadas apenas no X-Forwarded-For. Se a aplicação precisa do IP real, configure o Nginx para definir um header confiável usando $remote_addr:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
E remova qualquer versão fornecida pelo cliente desses headers antes de encaminhar:
proxy_set_header X-Forwarded-For "";
O motivo pelo qual isso vale a pena documentar não é a misconfiguration em si — é o que veio depois em um exercício real. O painel acessível expôs diagnósticos do sistema. Os diagnósticos expuseram fragmentos de log. Os logs expuseram um caminho de chave SSH temporária. A chave deu acesso a um segmento de rede interna.
Um header errado. Acesso completo à rede interna.
A superfície parecia pequena. Duas portas públicas, nenhum formulário de login óbvio, nenhum CMS. A fraqueza era invisível a um scan passivo. Ela só apareceu quando se prestou atenção em como o proxy se comportava sob diferentes entradas.
X-Forwarded-For ou X-Real-IP para controle de acesso na camada de borda — eles são controlados pelo clienteallow/deny, regras de firewall), não verificações de headerRelacionado: Case 01 — Linux Pivot via Reverse Proxy Misconfiguration