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

Origin retorna 200, mas o usuário recebe 403: como descobrir em qual camada está o bloqueio

Como comparar origin, WAF e CDN quando o backend responde 200, mas a URL pública recebe um 403 de challenge na borda.

·6 minutos

Uma URL pública começou a responder HTTP 403.

O primeiro impulso era olhar para a aplicação.

Só que a aplicação, quando testada diretamente no origin, respondia HTTP 200.

No mesmo ambiente, outra rota pública atrás de uma camada diferente também respondia 200.

A coleta externa ainda trazia uma pista forte:

1
2
cf-mitigated: challenge
server: cloudflare

Em outra resposta apareciam headers típicos de um WAF intermediário.

Esse é um daqueles casos em que perguntar “o site está fora?” é pouco útil.

A pergunta melhor é:

em qual camada o status mudou de 200 para 403?

O caminho real tinha mais de uma camada #

A topologia, de forma sanitizada, era parecida com:

1
2
3
4
5
6
7
8
9
cliente
  ↓
CDN / proteção de borda
  ↓
WAF intermediário
  ↓
Nginx origin
  ↓
aplicação

Quando existem várias camadas HTTP, um único curl na URL pública mostra só o resultado final.

Ele não prova que o Nginx gerou aquele status.

Também não prova que o WAF foi o responsável.

Muito menos que a aplicação devolveu 403.

O teste que mudou a investigação: origin direto #

No caso real, a mesma rota foi consultada diretamente no origin.

A resposta foi:

1
2
HTTP/1.1 200 OK
Server: nginx

Enquanto a URL pública que passava pela borda devolvia:

1
2
3
HTTP/2 403
cf-mitigated: challenge
server: cloudflare

Essa comparação não resolve automaticamente a política que causou o bloqueio.

Mas elimina uma hipótese importante:

o backend não estava simplesmente devolvendo 403 naquela requisição direta.

A mudança de status acontecia em algum ponto entre origin e cliente.

Status code é uma evidência de camada #

Eu gosto de pensar a investigação como uma sequência de checkpoints.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Aplicação responde?
      ↓
Nginx responde?
      ↓
Origin externo responde?
      ↓
WAF responde?
      ↓
CDN responde?
      ↓
Cliente recebe o quê?

Quando o status muda, a investigação ganha um limite bem mais útil.

Se:

1
2
origin = 200
público = 403

não faz sentido começar alterando PHP, banco ou código da aplicação sem uma evidência adicional.

Como testar sem desmontar a arquitetura #

Quando o origin é administrado por você e o teste é autorizado, dá para comparar a resposta preservando Host e, no HTTPS, o SNI.

Para HTTP:

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

Para HTTPS:

1
2
3
curl -i \
  --resolve app.example.com:443:ORIGIN_IP \
  https://app.example.com/caminho/

Isso evita um erro comum: acessar o IP direto e receber o vhost default, concluindo alguma coisa sobre uma aplicação que nem foi atendida naquele request.

Depois, salvar a resposta pública inteira #

Eu prefiro olhar headers completos:

1
2
3
curl -skD /tmp/public.headers \
  -o /tmp/public.body \
  https://app.example.com/caminho/

E comparar com o origin:

1
2
3
4
curl -skD /tmp/origin.headers \
  -o /tmp/origin.body \
  --resolve app.example.com:443:ORIGIN_IP \
  https://app.example.com/caminho/

A diferença pode mostrar:

  • status HTTP;
  • Server;
  • headers de challenge;
  • headers de WAF;
  • cookies de proteção;
  • redirects;
  • CSP;
  • cache;
  • headers adicionados/removidos.

O objetivo não é adivinhar a ferramenta pelo nome de um header.

É entender onde a resposta começou a mudar.

cf-mitigated: challenge era uma evidência bem específica #

Na resposta 403 do caso havia:

1
cf-mitigated: challenge

Isso é muito mais informativo do que apenas:

1
server: cloudflare

O primeiro indica que a resposta estava relacionada a um mecanismo de challenge/mitigação naquela borda.

O segundo, sozinho, só diz qual camada entregou a resposta ao cliente.

Essa distinção evita culpar o último hop por qualquer comportamento que ocorreu antes dele.

Outra rota respondia 200 através do WAF #

Na mesma coleta, uma URL pública que passava por outra composição de borda retornou:

1
2
HTTP/2 200
server: Sucuri/Cloudproxy

Isso não prova que “Sucuri funciona e Cloudflare não funciona”.

As rotas, políticas, hosts e regras podem ser diferentes.

Mas ajuda a confirmar que o problema não era uma indisponibilidade genérica do origin.

Havia comportamento HTTP diferente dependendo do caminho de publicação.

O erro mais fácil seria desligar a proteção para testar em produção #

Quando a borda entra como suspeita, uma tentação é remover proxy, desligar WAF ou mudar DNS de forma ampla.

Eu evitaria começar por aí.

Primeiro dá para coletar muita evidência de forma read-only:

1
2
3
4
5
6
7
8
DNS atual
headers públicos
status por rota
origin direto
logs Nginx
logs da aplicação
regras do WAF/CDN
Ray/request IDs quando disponíveis

Se for necessário mudar uma regra, fica muito mais fácil limitar a alteração depois de saber qual request está sendo bloqueado e por quê.

403 de borda não é o mesmo que 403 da aplicação #

Uma aplicação pode responder 403 por autenticação, RBAC ou regra de negócio.

Nginx pode responder 403 por acesso a arquivo/diretório ou configuração.

Um WAF pode responder 403 por assinatura de ataque.

Uma CDN pode responder 403 por challenge, firewall rule, bot rule ou política de segurança.

Para o usuário final, tudo isso pode parecer:

1
HTTP 403

Para troubleshooting, são problemas completamente diferentes.

Um roteiro que funciona bem #

Quando o origin parece saudável e a URL pública falha:

1. Resolver DNS #

1
dig +short app.example.com

2. Capturar a resposta pública #

1
curl -vkI https://app.example.com/caminho/

3. Testar o origin preservando hostname #

1
2
3
curl -vkI \
  --resolve app.example.com:443:ORIGIN_IP \
  https://app.example.com/caminho/

4. Comparar status e headers #

1
2
origin  = ?
público = ?

5. Correlacionar com logs do origin #

Se a requisição pública nem chega ao Nginx, isso é outra pista forte de que a resposta foi gerada antes.

6. Investigar a camada onde o comportamento divergiu #

Só então entrar em:

  • firewall rules;
  • WAF events;
  • bot/challenge;
  • rate limiting;
  • cache;
  • redirects;
  • política de path/hostname.

Por que eu tirei o “429” do título deste case #

A pauta original previa 403 ou 429 porque os dois status são comuns em borda.

A fonte técnica recuperada para este caso prova 403, inclusive com header de challenge.

Ela não prova um 429 nesta mesma ocorrência.

Então o artigo ficou mais estreito.

É uma decisão editorial simples:

melhor um título menor que a evidência sustenta do que uma cobertura maior inventada por plausibilidade.

O aprendizado #

O origin responder 200 não significa que o usuário vai receber 200.

E o usuário receber 403 não significa que a aplicação respondeu 403.

Em uma cadeia com CDN, WAF, reverse proxy e aplicação, o troubleshooting mais eficiente é comparar a mesma requisição em vários saltos e descobrir onde o status mudou pela primeira vez.

No caso analisado, o origin respondia 200 e a borda pública devolvia 403 com sinal explícito de challenge.

A partir daí, continuar mexendo na aplicação seria investigar a camada errada.

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.