Todos nós já passamos por isso: você envia uma requisição de API, espera pela resposta e, de repente, você se depara com o “erro de CORS” que aparece no console do seu navegador.
Para muitos desenvolvedores, o primeiro instinto é encontrar uma solução rápida: adicionar Access-Control-Allow-Origin: * e seguir em frente. No entanto, essa abordagem perde completamente o sentido. CORS não é apenas mais um obstáculo de configuração, mas um dos mecanismos de segurança de navegador mais importantes já criados.
CORS, ou Compartilhamento de Recursos de Origem Cruzada, existe para proteger os usuários, permitindo a comunicação legítima entre domínios entre aplicações web. No entanto, é frequentemente mal compreendido, mal configurado ou tratado como um bug a ser “bypassado”.
Mas não mais.
Neste guia, iremos além do básico. Você aprenderá:
- Por que o CORS existe e como ele evoluiu da Same-Origin Policy (SOP)
- Como navegadores e servidores realmente negociam o acesso de origem cruzada
- O que faz algumas configurações de CORS falharem, mesmo quando elas “aparecem corretas”
- Como lidar com requisições preflight, credenciais e peculiaridades do navegador de forma segura
Ao final, você não apenas saberá como configurar o CORS, mas também entenderá por que ele se comporta da maneira que se comporta e como projetar suas APIs de forma segura em torno dele.
O Que É CORS (e Por Que Existe)
CORS é um padrão de segurança do navegador que define como aplicações web de uma origem podem acessar recursos de outra de forma segura.
Para entender a segurança do CORS, você precisa primeiro saber por que ele foi criado.
Muito antes de APIs e microsserviços dominarem a web, os navegadores seguiam uma regra simples chamada Same-Origin Policy (SOP).
Essa política estabelecia que uma página web só poderia enviar e receber dados da mesma origem, ou seja, o mesmo protocolo, domínio e porta.
Por exemplo:
Essa restrição fazia todo o sentido no início da web, quando a maioria dos sites era monolítica. Um único site hospedava seu front-end, back-end e ativos, tudo sob um único domínio.
Mas, à medida que a web evoluiu com APIs, microsserviços e integrações de terceiros, essa mesma regra se tornou uma barreira. Os desenvolvedores precisavam que as aplicações front-end se comunicassem com outros domínios, como:
- www.example.com se comunicando com api.example.com
- Seu aplicativo conectando-se a uma CDN ou a um endpoint de analytics
- Clientes web chamando APIs de terceiros (como Stripe ou Google Maps)
A Same-Origin Policy tornou-se uma barreira que bloqueava arquiteturas modernas e distribuídas.
É aí que Cross-Origin Resource Sharing (CORS) entrou em cena.
Em vez de remover completamente as restrições do navegador, o CORS introduziu um relaxamento controlado da SOP. Ele criou uma forma segura para navegadores e servidores se comunicarem entre domínios, de forma segura e somente quando ambos os lados concordam.
Pense assim: a SOP é uma porta trancada que não deixa ninguém entrar, e o CORS é a mesma porta, mas com uma lista de convidados e um segurança verificando as identidades.
Esse equilíbrio entre flexibilidade e proteção é o que torna a configuração do CORS crítica para todo aplicativo web moderno.
Entendendo a Same-Origin Policy (SOP)
Antes de avançarmos na configuração do CORS, é essencial entender sua base: a Política de Mesma Origem (SOP).
Como afirmado anteriormente, a SOP é a primeira linha de defesa do navegador contra comportamentos maliciosos na web. Ela impede que um site acesse livremente os dados de outro, o que poderia expor informações sensíveis como cookies, tokens de autenticação ou detalhes pessoais.
Veja como funciona na prática: quando uma página da web é carregada no seu navegador, ela recebe uma origem baseada em três elementos: o protocolo, o host e a porta:
https:// api.example.com :443
^ ^ ^
protocolo host porta
Duas URLs são consideradas de mesma origem somente se todas essas três partes corresponderem. Caso contrário, o navegador as trata como de origem cruzada (cross-origin).
Essa regra simples impede ações maliciosas entre sites (cross-site). Sem ela, um site aleatório poderia carregar o painel do seu banco online em um frame invisível, ler seu saldo e enviá-lo a um invasor, tudo sem o seu consentimento.
Em resumo, a SOP existe para isolar conteúdo entre diferentes sites, garantindo que cada origem seja uma zona de segurança autocontida.
Por que a SOP Sozinha Não Era Suficiente
A Política de Mesma Origem (Same-Origin Policy) funcionava perfeitamente quando os sites eram autocontidos. Mas à medida que a web evoluiu para um ecossistema de APIs, microsserviços e arquiteturas distribuídas, essa regra estrita se tornou uma grande limitação.
Aplicações modernas precisavam:
- Chamar suas próprias APIs hospedadas em subdomínios diferentes (app.example.com → api.example.com)
- Buscar ativos de CDNs ou serviços de terceiros
- Integrar com APIs externas como Stripe, Firebase ou Google Maps
Sob a SOP, essas requisições legítimas de origem cruzada (cross-origin) eram bloqueadas. Desenvolvedores tentaram todas as soluções alternativas possíveis, incluindo JSONP, proxies reversos ou domínios duplicados, mas essas correções eram inseguras ou dolorosamente complexas.
Foi aí que o CORS (Cross-Origin Resource Sharing) mudou o jogo.
O CORS introduziu um sistema de handshake que permitiu que navegadores e servidores negociassem confiança. Em vez de quebrar a SOP, ele a estendeu, fornecendo uma maneira de permitir com segurança origens específicas para comunicação entre domínios.
Como o CORS Funciona: O Fluxo em Nível de Protocolo
Conforme declarado anteriormente, quando seu navegador faz uma requisição para uma origem diferente, ele não a envia cegamente. Em vez disso, ele segue um protocolo CORS bem definido: uma conversa de ida e volta entre o navegador e o servidor para determinar se a requisição deve ser permitida.
Em sua essência, o CORS funciona através de cabeçalhos HTTP. O navegador anexa um cabeçalho Origin a cada requisição cross-origin, informando ao servidor de onde a requisição está vindo. O servidor então responde com um ou mais cabeçalhos Access-Control-* que definem o que é permitido.
Aqui está um exemplo simplificado dessa conversa:
# Request
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
# Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
{"message": "Success"}
Neste caso, o servidor permite explicitamente que a origem https://app.example.com acesse seu recurso. O navegador verifica essa resposta, confirma a correspondência e entrega os dados ao seu JavaScript.
Mas se as origens não corresponderem ou se os cabeçalhos de resposta estiverem ausentes ou incorretos, o navegador bloqueia silenciosamente a resposta. Você não verá os dados, apenas aquela mensagem frustrante de “erro CORS” no seu console.
É importante notar que o CORS não torna um servidor mais seguro por si só. Em vez disso, ele impõe regras de engajamento entre navegadores e servidores, uma camada de segurança que garante que apenas origens confiáveis possam acessar recursos protegidos.
Tipos de Requisições CORS
O CORS define dois tipos principais de requisições: simples e preflighted. A diferença reside na quantidade de verificação que o navegador realiza antes de enviar os dados.
1. Requisições Simples
Uma requisição simples é o tipo mais direto. Ela é automaticamente permitida pelos navegadores, desde que siga regras específicas:
- Usa um destes métodos: GET, HEAD ou POST
- Inclui apenas certos cabeçalhos:
- Accept
- Accept-Language
- Content-Language
- Content-Type (mas apenas application/x-www-form-urlencoded, multipart/form-data ou text/plain)
- Não usa cabeçalhos personalizados ou streams
Veja como funciona:
# Request
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
# Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
{"message": "This is the response data"}Neste caso:
- O navegador adiciona automaticamente o cabeçalho Origin.
- O servidor deve retornar Access-Control-Allow-Origin com uma origem correspondente.
- Se a origem não corresponder ou estiver ausente, o navegador bloqueia a resposta.
2. Requisições Preflight
As coisas ficam mais interessantes com requisições não simples. Por exemplo, quando você usa métodos como PUT, DELETE ou cabeçalhos personalizados como Authorization.
Antes de enviar a requisição real, o navegador realiza uma verificação preflight usando uma requisição OPTIONS. Esta etapa garante que o servidor permite explicitamente a operação pretendida.
Aqui está um exemplo:
# Preflight Request
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization
# Preflight Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: PUT, POST, GET, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 3600
# Actual Request (only sent if preflight succeeds)
PUT /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token123
{"data": "update this resource"}Nesta sequência:
- O navegador detecta uma requisição não simples.
- Ele envia uma requisição OPTIONS preflight, pedindo permissão para o método e os cabeçalhos reais.
- O servidor responde com os métodos, cabeçalhos e origens que ele permite.
- Se a verificação preflight for aprovada, o navegador envia a requisição real. Caso contrário, ele a bloqueia.
Tratamento de Credenciais em CORS
Ao lidar com APIs que exigem autenticação como cookies, tokens ou logins baseados em sessão, o CORS se comporta de forma diferente.
Por padrão, os navegadores tratam requisições cross-origin como não autenticadas por motivos de segurança. Isso significa que cookies ou cabeçalhos de autenticação HTTP não são incluídos automaticamente.
Para habilitar requisições com credenciais de forma segura, duas etapas cruciais devem estar alinhadas:
1. O cliente deve permitir explicitamente as credenciais:
fetch('https://api.example.com/data', {
credentials: 'include'
})2. O servidor deve permiti-las explicitamente:
Access-Control-Allow-Credentials: true
Mas há um porém, e é um grande.
Quando Access-Control-Allow-Credentials é definido como true, você não pode usar um curinga (*) em Access-Control-Allow-Origin. Os navegadores rejeitarão a resposta se você tentar.
Isso ocorre porque permitir que todas as origens enviem requisições com credenciais anularia todo o propósito da segurança CORS, permitindo que qualquer site na internet acessasse dados privados vinculados à sessão de um usuário.
Então, em vez disso:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueVocê deve sempre usar uma origem específica:
Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Credentials: trueSe sua API atende a múltiplos domínios confiáveis, você pode retornar dinamicamente o cabeçalho de origem correto no lado do servidor:
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}Essa abordagem garante que suas requisições autenticadas permaneçam seguras e intencionais, não abertas a qualquer um que tente.
Como os Navegadores Determinam Quais Requisições São Elegíveis para CORS
Antes mesmo que uma requisição chegue ao seu servidor, o navegador decide se ela se enquadra nas regras do CORS.
Essa decisão depende da origem da requisição e se ela tem como alvo outro domínio, porta ou protocolo.
Por exemplo:
- Requisitando https://api.example.com de uma página servida em https://example.com: ✅ CORS se aplica (subdomínio diferente).
- Requisitando https://example.com:3000 de https://example.com: ✅ CORS se aplica (porta diferente).
- Requisitando https://example.com do mesmo domínio e porta: ❌ CORS não se aplica.
Se o navegador detecta que uma requisição cruza origens, ele inclui automaticamente o cabeçalho Origin na requisição:
Origin: https://example.com
Este cabeçalho informa ao servidor de onde a requisição veio e é o que o servidor usa para decidir se permite ou bloqueia o acesso.
Se a resposta não possui os cabeçalhos corretos (como Access-Control-Allow-Origin), o navegador simplesmente bloqueia o acesso à resposta, mesmo que o servidor tecnicamente tenha enviado uma.
Essa é uma distinção importante: o navegador impõe o CORS, não o servidor.
Verificações de Segurança Internas, XMLHttpRequest vs Fetch e Diferenças entre Navegadores
Nem todos os navegadores lidam com CORS da mesma forma, mas todos seguem o mesmo modelo de segurança: nunca confie em dados de origem cruzada a menos que explicitamente permitido.
O que difere é o quão estritamente eles aplicam as regras e a quais APIs eles as aplicam.
1. A verificação de segurança CORS interna
Quando um navegador recebe uma resposta a uma requisição de origem cruzada, ele executa uma etapa de validação interna antes de expor a resposta ao seu código JavaScript.
Ele verifica cabeçalhos como:
- Access-Control-Allow-Origin: deve corresponder à origem da requisição (ou ser * em alguns casos).
- Access-Control-Allow-Credentials: deve ser true se cookies ou tokens de autenticação estiverem envolvidos.
- Access-Control-Allow-Methods e Access-Control-Allow-Headers: devem corresponder à requisição preflight original, se uma tiver sido enviada.
Se alguma dessas verificações falhar, o navegador não gera um erro HTTP, ele simplesmente bloqueia o acesso à resposta e registra um erro CORS no console.
Isso torna a depuração complicada, porque a requisição de rede real ainda foi bem-sucedida, mas o navegador oculta o resultado por segurança.
2. XMLHttpRequest vs Fetch
Tanto XMLHttpRequest quanto a moderna API fetch() suportam CORS, mas se comportam de forma ligeiramente diferente quando se trata de credenciais e padrões.
Com XMLHttpRequest:
- Cookies e autenticação HTTP são enviados automaticamente se withCredentials for definido como true.
- O comportamento preflight depende se cabeçalhos personalizados são adicionados.
Com Fetch:
- Credenciais (cookies, autenticação HTTP) não são incluídas por padrão.
- Você deve habilitá-las explicitamente usando:
fetch("https://api.example.com/data", {
credentials: "include"
});- Fetch também trata os redirecionamentos de forma mais rigorosa sob CORS, pois não seguirá redirecionamentos de origem cruzada a menos que permitido.
Assim, embora fetch seja mais limpo e moderno, ele também é menos tolerante quando você esquece um cabeçalho ou perde uma regra de credencial.
3. Diferenças e peculiaridades dos navegadores
Embora a especificação CORS seja padrão, os navegadores a implementam com diferenças sutis:
- Safari pode ser excessivamente rigoroso com cookies e requisições credenciadas, especialmente quando cookies de terceiros são bloqueados.
- Firefox às vezes armazena em cache respostas preflight falhas por mais tempo do que o esperado, levando a resultados inconsistentes durante os testes.
- Chrome impõe CORS em certas cadeias de redirecionamento de forma mais agressiva do que outros.
Devido a essas diferenças, uma configuração que funciona perfeitamente em um navegador pode falhar silenciosamente em outro.
É por isso que é crucial testar as configurações de CORS em diferentes navegadores, especialmente quando credenciais ou redirecionamentos estão envolvidos.
Tratamento do Cabeçalho Origin no Lado do Servidor
Embora o navegador imponha CORS, a verdadeira tomada de decisão acontece no servidor.
Quando o navegador envia uma requisição cross-origin, ele sempre inclui o cabeçalho Origin. A função do servidor é inspecionar esse cabeçalho, decidir se deve permiti-lo e retornar os cabeçalhos CORS corretos na resposta.
1. Validando a Origem
Uma requisição típica pode chegar assim: Origin: https://frontend.example.com
No servidor, seu código precisa verificar se esta origem é permitida. A abordagem mais simples (e segura) é manter uma lista de permissões (allowlist) de domínios confiáveis:
const allowedOrigins = ["https://frontend.example.com", "https://admin.example.com"];
if (allowedOrigins.includes(req.headers.origin)) {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
}Isso garante que apenas clientes conhecidos possam acessar sua API, enquanto outros não recebem permissão CORS.
Evite retornar Access-Control-Allow-Origin: * se sua API lida com cookies, tokens ou outras credenciais.
2. Tratamento de Requisições Preflight
Para requisições preflight OPTIONS, o servidor deve responder com o mesmo cuidado que para as requisições principais.
Uma resposta preflight completa inclui:
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400Esses cabeçalhos informam ao navegador o que é permitido e por quanto tempo ele pode armazenar essa decisão em cache. Se algum deles estiver faltando ou incorreto, o navegador bloqueará a requisição subsequente, mesmo que seu endpoint funcione perfeitamente.
3. Definindo Dinamicamente os Cabeçalhos CORS
Em sistemas grandes (como plataformas multi-tenant ou APIs com múltiplos clientes), as origens permitidas podem precisar ser dinâmicas.
Por exemplo:
const origin = req.headers.origin;
if (origin && origin.endsWith(".trustedclient.com")) {
res.setHeader("Access-Control-Allow-Origin", origin);
}Este padrão permite todos os subdomínios de um domínio confiável, enquanto ainda filtra fontes desconhecidas.
Apenas certifique-se de validar as origens cuidadosamente e não faça string-match em entrada de usuário sem restrições, ou atacantes poderiam forjar cabeçalhos que parecem válidos.
4. Por que “Funciona no Postman” Não Significa Que Está Configurado Corretamente
Um dos maiores equívocos sobre CORS é este: “Funciona no Postman, então deve ser um problema do navegador.”
O Postman não impõe CORS de forma alguma, porque não é um navegador.
Isso significa que mesmo uma API completamente aberta, sem cabeçalhos Access-Control-*, responderá bem lá, mas falhará imediatamente no Chrome ou Firefox.
Então, se sua API funciona no Postman, mas não em seu aplicativo web, seus cabeçalhos CORS estão provavelmente incompletos ou mal configurados.
Configurações Incorretas Comuns de CORS (e Como Evitá-las)
1. Usando Access-Control-Allow-Origin: * com Credenciais
Este é o erro mais frequente e perigoso.
Se sua resposta incluir ambos:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true…o navegador bloqueará a requisição automaticamente.
A especificação CORS proíbe o uso de curingas quando credenciais são incluídas, pois isso permitiria que qualquer site acessasse dados do usuário vinculados a cookies ou tokens de autenticação.
Correção: Sempre retorne uma origem específica quando credenciais são usadas:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true2. Esquecer de Lidar com Requisições Preflight
Muitas APIs respondem corretamente a GET e POST, mas esquecem da requisição preflight OPTIONS.
Quando isso acontece, o navegador nunca alcança seu endpoint real, e ele bloqueia a requisição principal após o preflight falho.
Correção: Lide explicitamente com requisições OPTIONS e responda com os cabeçalhos corretos:
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
return res.sendStatus(204);
}3. Cabeçalhos de Requisição e Resposta desalinhados
Outro problema sutil: sua requisição preflight pode pedir por certos cabeçalhos, mas o servidor não os permite explicitamente.
Por exemplo, se sua requisição inclui:
Access-Control-Request-Headers: Authorization, Content-Type
…mas o servidor só responde com:
Access-Control-Allow-Headers: Content-Type
...o navegador o bloqueia. Ambas as listas devem corresponder exatamente.
Correção: Certifique-se de que seu Access-Control-Allow-Headers inclua todos os cabeçalhos que o cliente possa enviar, especialmente Authorization, Accept e cabeçalhos personalizados.
4. Retornando Múltiplos Access-Control-Allow-Origin Headers
Alguns proxies ou frameworks mal configurados enviam múltiplos Access-Control-Allow-Origin headers (por exemplo, um estático * e uma origem dinâmica).
Os navegadores tratam isso como inválido e bloqueiam a requisição completamente.
Correção: Sempre retorne um único e válido Access-Control-Allow-Origin header.
5. Esquecendo as Restrições de Método
Se você não incluir todos os métodos permitidos em Access-Control-Allow-Methods, os navegadores rejeitarão requisições legítimas.
Por exemplo, uma API pode suportar PUT, mas sua resposta de preflight permite apenas GET e POST.
Correção: Liste cada método suportado ou faça a correspondência dinâmica de suas rotas de API para garantir a consistência.
6. Ignorando Respostas de Preflight em Cache
Navegadores modernos armazenam em cache os resultados de preflight para otimizar o desempenho.
Mas se seu servidor ou CDN armazena respostas em cache sem variar por Origin, você pode acidentalmente enviar os cabeçalhos CORS errados para outro cliente.
Correção: Utilize o cabeçalho Vary: Origin para garantir que as respostas sejam armazenadas em cache separadamente por origem.
Problemas de CORS raramente vêm de um grande erro. Geralmente, são o resultado de várias pequenas inconsistências entre as expectativas do navegador e a configuração do servidor. Compreender esses padrões ajuda a evitar ciclos intermináveis de depuração de “erros de CORS”.
CORS Não é o Inimigo, o Inimigo é Não Entendê-lo
À primeira vista, o CORS parece uma barreira desnecessária, ou mais como um guardião que quebra suas requisições e retarda o desenvolvimento.
Mas, na realidade, é uma das funcionalidades de segurança de navegador mais importantes já criadas.
Uma vez que você entende como funciona, você para de ver “erros de CORS” como falhas aleatórias, e em vez disso, eles se tornam sinais de que seu cliente e servidor precisam se alinhar melhor em termos de confiança, cabeçalhos ou credenciais.
Seja você construindo um aplicativo de página única ou um ecossistema de API distribuído, o CORS é seu aliado para manter os usuários seguros enquanto permite a comunicação segura entre domínios.
Então, da próxima vez que você se deparar com aquela mensagem familiar no console, não recorra ao curinga. Leia os cabeçalhos, trace a lógica e deixe que seu entendimento, e não um hack aleatório, guie a correção!

