Voltar para Artigos
Back-end8 min de leitura

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.

12 de agosto de 2026

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 origem
  • https://app.com → https://api.app.com ❌ Subdomínio diferente
  • https://app.com → http://app.com ❌ Protocolo diferente
  • https://app.com → https://app.com:4000 ❌ Porta diferente
  • https://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:

  1. Navegador envia OPTIONS /api/users com headers Origin, Access-Control-Request-Method e Access-Control-Request-Headers
  2. Servidor responde com Access-Control-Allow-Origin, Access-Control-Allow-Methods e Access-Control-Allow-Headers
  3. Se a resposta autorizar, o navegador envia a requisição real (POST, PUT, etc.)
  4. 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

bash
npm install cors
npm install -D @types/cors
cors.ts
import 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,
};
server.ts
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());
// ... rotas

CORS e Cookies: credentials: true

Se você usa cookies HttpOnly para o Refresh Token, há dois requisitos obrigatórios que muitos desenvolvedores esquecem:

  1. No servidor: credentials: true na config do cors. Com credentials: true, o origin *não pode ser `''`** — precisa ser um domínio específico.
  2. No cliente: withCredentials: true no Axios (ou credentials: 'include' no fetch). Sem isso, o navegador não envia nem recebe cookies cross-origin.
Configuração do cliente (Axios)
// 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=Strict ou Lax) 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.
Configuração de cookies com SameSite
// 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

.env.development
# Desenvolvimento: permite localhost em múltiplas portas
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
.env.production
# Produção: apenas domínios reais
ALLOWED_ORIGINS=https://app.suaempresa.com,https://painel.suaempresa.com

Nunca 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.