↓Pular para o conteúdo principal
  1. blog/
ARTIGO TÉCNICO

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.

·6 minutos

Uma resposta HTTP chegou com isto:

1
2
x-frame-options: SAMEORIGIN
x-frame-options: DENY

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:

1
SAMEORIGIN

permite framing apenas pela mesma origem.

Já:

1
DENY

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:

1
curl -I -L --max-time 20 https://example.com/

E a resposta incluía, entre outros:

1
2
3
4
5
6
7
8
9
x-frame-options: SAMEORIGIN
x-frame-options: DENY
x-content-type-options: nosniff
strict-transport-security: ...
content-security-policy: upgrade-insecure-requests;
content-security-policy: frame-ancestors 'none'
server: cloudflare
x-sucuri-id: ...
x-sucuri-cache: MISS

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:

1
2
3
4
5
6
7
8
9
aplicação
   ↓
Nginx/origin
   ↓
WAF
   ↓
CDN
   ↓
cliente

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:

1
nginx -T 2>/dev/null | grep -niE 'X-Frame-Options|frame-ancestors'

Também vale procurar no filesystem de configuração:

1
2
grep -RniE 'X-Frame-Options|frame-ancestors' \
  /etc/nginx 2>/dev/null

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:

1
2
3
curl -I \
  -H 'Host: example.com' \
  http://ORIGIN_IP/

Para HTTPS, quando o certificado do origin cobre o domínio:

1
2
3
curl -I \
  --resolve example.com:443:ORIGIN_IP \
  https://example.com/

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:

1
2
SAMEORIGIN
DENY

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:

1
2
3
4
5
6
7
8
curl -skD /tmp/origin.headers -o /dev/null \
  --resolve example.com:443:ORIGIN_IP \
  https://example.com/

curl -skD /tmp/public.headers -o /dev/null \
  https://example.com/

diff -u /tmp/origin.headers /tmp/public.headers

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:

1
curl -I http://127.0.0.1:<porta>/

ou o endpoint interno equivalente.

A sequência passa a ser:

1
2
3
4
5
backend direto
→ Nginx/origin
→ WAF
→ CDN
→ público

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:

1
Content-Security-Policy: frame-ancestors 'none'

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:

1
2
3
aplicação → SAMEORIGIN
Nginx      → DENY
WAF        → frame-ancestors 'none'

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:

1
2
3
4
5
6
7
server {
    add_header X-Frame-Options "SAMEORIGIN" always;

    location / {
        ...
    }
}

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 #

1
2
3
4
5
6
7
8
9
[ ] salvar a resposta pública completa
[ ] nginx -T e grep por header/CSP
[ ] testar backend direto, se possível
[ ] testar origin mantendo Host/SNI
[ ] comparar origin x público
[ ] identificar onde cada header aparece pela primeira vez
[ ] escolher uma camada dona da política
[ ] remover duplicidade sem quebrar outros vhosts
[ ] validar novamente pela URL pública

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?

TEM UM CENÁRIO PARECIDO?

Me chama.

Manda o contexto, os sintomas e o que já foi testado. Bora organizar as evidências antes de sair mexendo.

Falar com Castro →
Sem enrolação. Com evidência.