Programação Por Equipe DevBras

Capa do artigo: CORS libera o navegador de outro site. Não é a trava do seu servidor

CORS libera o navegador de outro site. Não é a trava do seu servidor

Entenda o que CORS faz de verdade, configure uma lista explícita de origens com credenciais e mantenha a autenticação e a autorização no servidor, onde elas valem para qualquer cliente.

O front em app.exemplo.com.br não conseguia chamar a API em api.exemplo.com.br. Alguém achou na internet a configuração que "resolve CORS", que devolve no cabeçalho a mesma origem que chegou e libera credenciais. Funcionou. Também passou a permitir que qualquer site, aberto no navegador de uma pessoa logada, leia os dados dela na sua API. A resposta curta: CORS não protege o seu servidor. É uma regra que o navegador aplica para decidir se um script de outro site pode ler a resposta. Configure uma lista explícita de origens, nunca reflita a origem recebida quando houver cookie, e mantenha autenticação e autorização em cada rota, porque clientes fora do navegador ignoram CORS completamente.

O que o CORS realmente faz

Navegadores aplicam a política de mesma origem: um script carregado de site-a.example pode até disparar pedidos para site-b.example, mas não pode ler a resposta. Origem é a combinação de esquema, host e porta. https://app.exemplo.com.br e https://api.exemplo.com.br são origens diferentes.

CORS é o mecanismo pelo qual o servidor afrouxa essa política. Ele responde com Access-Control-Allow-Origin dizendo qual origem pode ler. Se o script quiser mandar cookies junto (credentials: 'include' no fetch), o servidor também precisa responder Access-Control-Allow-Credentials: true, e aí a origem precisa ser exata: o navegador recusa * com credenciais.

Repare no que isso significa. Com CORS restrito, o pedido pode chegar ao seu servidor do mesmo jeito. Pedidos considerados simples, como um POST de formulário, saem sem consulta prévia; o navegador só esconde a resposta do script. Pedidos não simples, como um PUT com JSON, passam antes por uma consulta OPTIONS (o preflight), e o navegador só envia o pedido real se a resposta autorizar. Em nenhum caso o CORS impede o curl, um script Python ou um app de celular de chamar a sua API com o que quiserem.

Então o CORS protege o usuário, não o servidor. Ele impede que um site malicioso, rodando no navegador de alguém logado no seu sistema, leia as respostas da sua API com o cookie dessa pessoa. Liberar errado desfaz justamente essa proteção.

Por que refletir a origem é perigoso

A configuração "que resolve" lê o cabeçalho Origin do pedido e devolve o mesmo valor em Access-Control-Allow-Origin, com Allow-Credentials: true. Para o navegador, isso equivale a dizer: qualquer site pode ler as respostas autenticadas desta API. Uma página qualquer, visitada pela pessoa logada, faz fetch('https://api.exemplo.com.br/perfil', { credentials: 'include' }) e recebe nome, e-mail, endereço, o que a rota devolver.

O mesmo vale para validações frouxas: expressão regular que aceita qualquer coisa terminando em exemplo.com.br aceita golpeexemplo.com.br. Liberar a origem null também é arriscado, porque iframes com sandbox e alguns redirecionamentos produzem essa origem, e um atacante consegue gerá-la.

Como configurar CORS com lista explícita

  1. Liste as origens que realmente consomem a API pelo navegador, com esquema e sem barra no final.
  2. Compare a origem recebida por igualdade exata com essa lista.
  3. Só envie Access-Control-Allow-Credentials: true se o front de fato usa cookie na API.
  4. Se o valor de Access-Control-Allow-Origin muda conforme o pedido, envie Vary: Origin, para que caches não sirvam a resposta de uma origem para outra.
  5. Restrinja métodos e cabeçalhos ao que o front usa.
  6. Mantenha em toda rota a verificação de sessão e de permissão, como se o CORS não existisse.

Em Express com o pacote cors:

const express = require('express');
const cors = require('cors');

const app = express();
const ORIGENS = new Set([
  'https://app.exemplo.com.br',
  'https://admin.exemplo.com.br'
]);

app.use(cors({
  origin: function (origem, callback) {
    callback(null, Boolean(origem) && ORIGENS.has(origem));
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE']
}));

app.get('/perfil', exigirLogin, (req, res) => {
  res.json(buscarPerfil(req.usuario.sub));
});

Quando a função devolve false, o pacote não envia os cabeçalhos de CORS. O pedido ainda é processado pela rota; quem bloqueia a leitura é o navegador. Por isso o exigirLogin continua ali. Evite origin: true nesse pacote: ele reflete qualquer origem recebida.

Para testar sem navegador, simule uma origem estranha e veja o que volta:

curl -s -D - -o /dev/null -H "Origin: https://site-qualquer.example" https://api.exemplo.com.br/perfil

Se a resposta trouxer Access-Control-Allow-Origin: https://site-qualquer.example, a API está refletindo a origem. Repita com a sua origem legítima para confirmar que ela aparece. Se a API está atrás de Nginx ou de um CDN, confira se alguma dessas camadas também adiciona cabeçalhos de CORS: duas camadas configurando o mesmo cabeçalho costumam gerar valores duplicados, que o navegador recusa, ou uma regra permissiva escondida que ninguém lembra de ter criado.

CORS, cookie e CSRF

Como o CORS não impede o envio de pedidos simples, ele não substitui a proteção contra CSRF. Um formulário em outro site ainda consegue fazer POST na sua API; o que segura esse pedido é o cookie com SameSite, como explicado em cookie de sessão com HttpOnly, e token anti-CSRF nas ações sensíveis. Exigir Content-Type: application/json ajuda, porque força o preflight, mas não é motivo para dispensar o resto.

Erros comuns

  • Refletir a origem com credenciais. Qualquer site lê os dados da pessoa logada na sua API.
  • Validar origem com regex ou endsWith. Domínios registrados por terceiros com o seu nome no final passam.
  • Liberar a origem null. Um atacante consegue produzir essa origem com iframe em sandbox.
  • Achar que CORS bloqueia o pedido. A ação já aconteceu no servidor quando o navegador esconde a resposta.
  • Tirar autenticação de rota porque "só o nosso front chama". Qualquer cliente fora do navegador chama, e CORS não participa.
  • Esquecer Vary: Origin em configuração manual. Um cache entrega a resposta com a origem errada e o front quebra de forma intermitente.

Checklist

  • A lista de origens permitidas está escrita no código ou na configuração, uma a uma.
  • O teste com curl e origem estranha não devolve Access-Control-Allow-Origin com ela.
  • A origem null não é aceita.
  • Allow-Credentials só está ligado se o front usa cookie.
  • Toda rota confere sessão e permissão por conta própria.
  • Ações que mudam dados têm proteção contra CSRF além do CORS.

Rode o curl acima contra a sua API de produção, trocando o domínio. Qual origem, além das suas, recebe permissão de ler resposta com cookie?

Avalie este artigo

Referência: https://developer.mozilla.org/docs/Web/HTTP/CORS