CORS Explicado: Como Proteger sua API de Invasores Web
Same-Origin Policy e CORS do zero: preflight requests, headers de resposta, whitelist com variáveis de ambiente, credenciais e cookies, e a relação com CSRF.
O erro de CORS é um dos mais frustrantes para quem está começando no desenvolvimento web — não porque é difícil de corrigir, mas porque não é intuitivo de entender. O erro não vem do seu servidor, vem do navegador. E a solução mais comum que você vê no Stack Overflow — origin: '*' — funciona mas abre um buraco de segurança significativo.
Neste artigo vamos do mecanismo fundamental (Same-Origin Policy) até a configuração correta para produção: whitelist dinâmica, preflight requests, headers necessários, e a relação com cookies e CSRF.
Same-Origin Policy: A Raiz do Problema
O navegador implementa a Same-Origin Policy — uma regra de segurança que impede que JavaScript em https://site-pirata.com faça requisições para https://api.suaempresa.com. A origem é a combinação de protocolo + domínio + porta. Qualquer diferença em um desses três campos = origens diferentes = bloqueio por padrão.
https://app.com→https://app.com/api✅ Mesma origemhttps://app.com→https://api.app.com❌ Subdomínio diferentehttps://app.com→http://app.com❌ Protocolo diferentehttps://app.com→https://app.com:4000❌ Porta diferentehttps://app.com→https://outro.com❌ Domínio diferente
O CORS (Cross-Origin Resource Sharing) é o mecanismo pelo qual o servidor pode flexibilizar essa política, dizendo ao navegador: 'Eu autorizo requisições vindas da origem X'. Sem esse header, o navegador bloqueará a resposta antes de entregá-la ao JavaScript — mesmo que o servidor tenha processado e respondido.
CORS não protege o seu servidor — protege os usuários. O servidor processa a requisição normalmente. É o navegador que decide se entrega ou não a resposta ao JavaScript da página. Ferramentas como Postman e curl não são navegadores e por isso ignoram CORS completamente.
Preflight Requests: O que Acontece nos Bastidores
Para requisições que modificam dados (POST, PUT, DELETE) ou que enviam headers customizados (Authorization, Content-Type: application/json), o navegador faz uma preflight request automática antes da requisição real:
- Navegador envia
OPTIONS /api/userscom headersOrigin,Access-Control-Request-MethodeAccess-Control-Request-Headers - Servidor responde com
Access-Control-Allow-Origin,Access-Control-Allow-MethodseAccess-Control-Allow-Headers - Se a resposta autorizar, o navegador envia a requisição real (POST, PUT, etc.)
- Se não autorizar, o navegador bloqueia e você vê o erro de CORS
Isso significa que em produção você terá o dobro de requisições para endpoints não-simples. O header Access-Control-Max-Age resolve isso cacheando o resultado do preflight:
Configuração Correta no Express
npm install cors
npm install -D @types/corsimport type { CorsOptions } from 'cors';
// Lista de origens permitidas — carrega do ambiente
// Em produção: ALLOWED_ORIGINS=https://app.com,https://painel.app.com
const allowedOrigins = (
process.env.ALLOWED_ORIGINS ?? 'http://localhost:3000'
)
.split(',')
.map((origin) => origin.trim());
export const corsOptions: CorsOptions = {
origin: (origin, callback) => {
// origin é undefined para:
// - Requisições do mesmo domínio (raras)
// - Apps mobile, Postman, curl
// - Webhooks de serviços externos
// Permitimos essas, mas revise se sua API é puramente para browsers
if (!origin) {
callback(null, true);
return;
}
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origem ${origin} não autorizada pelo CORS.`));
}
},
// OBRIGATÓRIO para cookies HttpOnly (Refresh Token) funcionarem
credentials: true,
// Métodos HTTP permitidos
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
// Headers que o cliente pode enviar
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
// Headers que o cliente pode ler na resposta
exposedHeaders: ['X-Total-Count', 'X-Page'],
// Cache do preflight: 24 horas (reduz OPTIONS requests)
maxAge: 86400,
};import express from 'express';
import cors from 'cors';
import { corsOptions } from './config/cors';
const app = express();
// CORS deve vir ANTES de qualquer rota
// O método OPTIONS do preflight precisa ser tratado globalmente
app.use(cors(corsOptions));
// Para preflight requests em todas as rotas:
app.options('*', cors(corsOptions));
app.use(express.json());
// ... rotasCORS e Cookies: credentials: true
Se você usa cookies HttpOnly para o Refresh Token, há dois requisitos obrigatórios que muitos desenvolvedores esquecem:
- No servidor:
credentials: truena config do cors. Comcredentials: true, oorigin*não pode ser `''`** — precisa ser um domínio específico. - No cliente:
withCredentials: trueno Axios (oucredentials: 'include'no fetch). Sem isso, o navegador não envia nem recebe cookies cross-origin.
// Necessário para cookies HttpOnly (Refresh Token) funcionarem
export const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true, // ← OBRIGATÓRIO para cookies cross-origin
});
// Equivalent em fetch:
// fetch(url, { credentials: 'include' })CORS e CSRF: Relação e Diferenças
CORS e CSRF (Cross-Site Request Forgery) são conceitos relacionados mas distintos:
- CORS previne que JavaScript em outro domínio leia a resposta de uma requisição à sua API. Mas não previne que um site malicioso submeta um formulário HTML para sua API — formulários não são afetados por CORS.
- CSRF ataca via formulários ou tags de imagem que submetem requisições sem JavaScript. A proteção é o SameSite Cookie (
SameSite=StrictouLax) ou um token CSRF no header. - SameSite=Strict impede que o cookie seja enviado em qualquer requisição cross-origin — incluindo formulários. É a defesa mais forte contra CSRF.
// Refresh Token com proteção CSRF via SameSite
res.cookie('refresh_token', refreshToken, {
httpOnly: true, // Não acessível via JavaScript (XSS)
secure: true, // Apenas HTTPS
sameSite: 'strict', // Não enviado em requisições cross-origin (CSRF)
maxAge: 30 * 24 * 60 * 60 * 1000,
path: '/auth/refresh', // Enviado apenas para esta rota
});
// NOTA: sameSite: 'strict' + credentials: true no CORS funcionam juntos.
// O browser só envia o cookie quando a requisição vem do domínio autorizado.Testando CORS com Variáveis de Ambiente
# Desenvolvimento: permite localhost em múltiplas portas
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001# Produção: apenas domínios reais
ALLOWED_ORIGINS=https://app.suaempresa.com,https://painel.suaempresa.comNunca use `origin: '' com credentials: true** — o navegador vai rejeitar com erro. E nunca use origin: '*'` em uma API que lida com dados sensíveis de usuários autenticados. Qualquer site da internet poderia fazer requisições em nome dos seus usuários.
Conclusão
CORS bem configurado protege seus usuários de ataques de cross-origin JavaScript enquanto permite que seus próprios frontends se comuniquem com a API. A configuração correta usa uma whitelist de origens carregada de variáveis de ambiente, credentials: true para cookies, maxAge para cachear preflights, e SameSite=Strict nos cookies para complementar a proteção contra CSRF.