Dois X-Frame-Options diferentes na mesma resposta: Nginx, Sucuri ou Cloudflare?
Como investigar uma resposta HTTP que chega com X-Frame-Options SAMEORIGIN e DENY ao mesmo tempo sem culpar a camada errada.
Uma resposta HTTP chegou com isto:
| |
No mesmo request.
No caminho também apareciam sinais de mais de uma camada intermediária, incluindo headers de WAF/CDN e o servidor de borda.
A pergunta parecia simples:
quem está adicionando o X-Frame-Options duplicado?
Nginx? WAF? CDN? Aplicação?
O erro seria responder só olhando o header final.
Primeiro: os dois valores não significam a mesma coisa #
X-Frame-Options controla se a página pode ser carregada dentro de um frame.
Os dois valores encontrados representam políticas diferentes:
| |
permite framing apenas pela mesma origem.
Já:
| |
bloqueia framing.
Receber os dois na mesma resposta significa, no mínimo, que mais de uma configuração está tentando controlar a mesma política — ou que alguma camada está preservando um valor e adicionando outro.
Isso é um problema de governança de header antes mesmo de ser um problema de navegador.
O curl -I mostrou o sintoma, não o autor #
A coleta externa sanitizada era parecida com:
| |
E a resposta incluía, entre outros:
| |
Isso prova que o cliente recebeu os dois X-Frame-Options.
Também mostra que havia sinais de múltiplas camadas no caminho.
Mas não prova qual delas gerou cada valor.
server: cloudflare não transforma todo header da resposta em header criado pelo Cloudflare.
Da mesma forma, um x-sucuri-id não permite atribuir automaticamente todos os demais headers ao Sucuri.
A investigação precisa comparar pontos do caminho #
O método mais útil é observar a mesma aplicação em diferentes posições da cadeia.
Conceitualmente:
| |
Se eu só vejo o último ponto, sei o que chegou ao usuário, mas não sei onde o header apareceu.
1. Começar pela configuração do origin #
No Nginx, procurar qualquer definição de header:
| |
Também vale procurar no filesystem de configuração:
| |
Isso responde se o origin está configurado para adicionar a política.
Ainda não responde se alguma aplicação atrás dele também manda o header.
2. Testar o origin diretamente, mantendo o Host #
Quando existe autorização e um endereço de origin conhecido, uma comparação útil é enviar a requisição diretamente para ele mantendo o hostname HTTP/TLS correto.
Exemplo para HTTP:
| |
Para HTTPS, quando o certificado do origin cobre o domínio:
| |
O objetivo não é “bypassar segurança” como solução permanente.
É comparar, em ambiente que você administra, o que sai do origin com o que chega pela cadeia pública.
Se o origin já devolve:
| |
a duplicidade nasceu antes da borda.
Se o origin devolve apenas um e a resposta pública devolve dois, a segunda política foi adicionada em alguma camada intermediária.
3. Comparar headers completos, não só X-Frame-Options #
Eu prefiro salvar as duas respostas:
| |
Isso costuma revelar mais do que buscar uma linha isolada.
Você pode encontrar diferenças em:
- CSP;
- HSTS;
- cookies;
- cache;
- headers de WAF;
- headers de CDN;
- redirects;
Server;- framing.
A cadeia HTTP deixa marcas.
4. Não esquecer da aplicação #
Mesmo que nginx -T não mostre add_header X-Frame-Options, o backend pode estar gerando o header.
Por isso, se possível, vale testar o upstream diretamente na rede local:
| |
ou o endpoint interno equivalente.
A sequência passa a ser:
| |
Quando o valor aparece pela primeira vez, você encontrou a camada responsável pela introdução daquele header.
frame-ancestors também estava na resposta #
O caso tinha ainda:
| |
Isso adiciona mais uma política de framing à resposta.
Em navegadores modernos, frame-ancestors dentro de CSP é a ferramenta mais expressiva para esse controle.
O ponto operacional é evitar ter políticas contraditórias espalhadas em três lugares diferentes.
Se a intenção é bloquear framing completamente, por exemplo, faz mais sentido que a arquitetura tenha uma política deliberada e documentada do que:
| |
mesmo que o resultado final pareça “mais seguro”.
Segurança duplicada sem ownership claro vira configuração difícil de manter.
Cuidado com add_header no Nginx #
Outro detalhe comum é herança.
Uma configuração pode ter headers definidos em níveis diferentes:
| |
Enquanto outro include ou proxy já preserva um header vindo do upstream.
Se a decisão é o Nginx ser o ponto central dessa política, talvez seja necessário remover/ocultar o header anterior antes de adicionar o valor canônico — mas essa mudança depende da arquitetura e precisa ser validada, não copiada cegamente.
O que dava para concluir no caso #
Com a resposta externa, era seguro afirmar:
- o usuário recebia dois valores de
X-Frame-Options; - os valores eram diferentes;
- havia mais de uma camada HTTP no caminho;
- CSP também continha política de
frame-ancestors; - era necessário comparar origin e borda para atribuir autoria.
O que não era seguro afirmar só com aquele curl:
- “o Cloudflare adicionou
DENY”; - “o Sucuri adicionou
SAMEORIGIN”; - “o Nginx é o culpado”;
- “a aplicação gera o header”.
Qualquer uma dessas frases exigia observação em outro ponto da cadeia.
Checklist que eu uso para header duplicado #
| |
O aprendizado #
Header duplicado parece um detalhe cosmético até o dia em que ninguém sabe qual camada é responsável pela política de segurança.
O curl externo mostrou o problema.
A solução metodológica foi não confundir o último servidor que respondeu ao cliente com o componente que criou cada header.
Em ambiente com CDN, WAF, reverse proxy e aplicação, a pergunta certa não é:
Quem parece ser o culpado?
É:
em qual salto esse header apareceu pela primeira vez?
Me chama.
Manda o contexto, os sintomas e o que já foi testado. Bora organizar as evidências antes de sair mexendo.