VULNEXUSAI · BLOG

Erro de CORS: como resolver no Node.js e Nginx sem usar wildcard

Erro de CORS bloqueando sua API em produção? Veja como configurar Access-Control-Allow-Origin corretamente no Express, Nginx e Next.js — sem usar * e sem quebrar o cache de CDN.

Publicado em 01 de setembro de 2026

Se o console do navegador mostra Access to fetch at '...' from origin '...' has been blocked by CORS policy, o problema não está no frontend — está na configuração do servidor. O browser está fazendo exatamente o que deve: bloqueando uma resposta que o servidor não autorizou explicitamente.

Por que o erro aparece só em produção

Em desenvolvimento você acessa tudo via localhost, então o mesmo servidor responde o frontend e a API — sem CORS. Em produção, o frontend fica em app.meusite.com e a API em api.meusite.com (ou meusite.com/api). São origens diferentes. O browser exige o header Access-Control-Allow-Origin correto; sem ele, bloqueia.

Se quiser entender o mecanismo completo por trás desse bloqueio antes de mexer na configuração, veja o que é CORS e como ele funciona.

Node.js / Express com allowlist de origens

Nunca use Access-Control-Allow-Origin: * em rotas autenticadas — cookies e headers de Authorization não são enviados com wildcard. Use uma allowlist:

const ALLOWED_ORIGINS = [
  'https://meuapp.com',
  'https://www.meuapp.com',
  'https://staging.meuapp.com',
];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (ALLOWED_ORIGINS.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin'); // evita cache incorreto na CDN
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader(
      'Access-Control-Allow-Headers',
      'Content-Type, Authorization'
    );
    res.setHeader(
      'Access-Control-Allow-Methods',
      'GET, POST, PUT, DELETE, OPTIONS'
    );
  }
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  next();
});

O header Vary: Origin é obrigatório sempre que você reflete a origem dinamicamente: sem ele, uma CDN pode cachear a resposta com o Access-Control-Allow-Origin de um cliente e servir para outro, causando erros silenciosos.

Nginx com map (sem if dentro do location)

A documentação do Nginx desaconselha if dentro de blocos location. Use map no nível http:

map $http_origin $cors_origin {
  default "";
  "https://meuapp.com"         $http_origin;
  "https://www.meuapp.com"     $http_origin;
  "https://staging.meuapp.com" $http_origin;
}

server {
  listen 443 ssl;
  server_name api.meuapp.com;

  location / {
    if ($request_method = OPTIONS) {
      add_header Access-Control-Allow-Origin  $cors_origin always;
      add_header Vary                         Origin        always;
      add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
      add_header Access-Control-Allow-Headers "Content-Type, Authorization"     always;
      add_header Access-Control-Max-Age       86400;
      return 204;
    }

    add_header Access-Control-Allow-Origin      $cors_origin always;
    add_header Vary                             Origin        always;
    add_header Access-Control-Allow-Credentials true          always;

    proxy_pass http://localhost:3000;
  }
}

Next.js (App Router) com Route Handler dinâmico

// app/api/dados/route.ts
const ALLOWED_ORIGINS = [
  'https://meuapp.com',
  'https://www.meuapp.com',
];

function corsHeaders(origin: string | null) {
  const allowed = origin && ALLOWED_ORIGINS.includes(origin) ? origin : '';
  return {
    'Access-Control-Allow-Origin': allowed,
    'Vary': 'Origin',
    'Access-Control-Allow-Credentials': 'true',
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  };
}

export async function OPTIONS(req: Request) {
  const origin = req.headers.get('origin');
  return new Response(null, { status: 204, headers: corsHeaders(origin) });
}

export async function GET(req: Request) {
  const origin = req.headers.get('origin');
  const data = { ok: true };
  return Response.json(data, { headers: corsHeaders(origin) });
}

Erros comuns que quebram a produção

Wildcard com credenciaisAccess-Control-Allow-Origin: * combinado com Access-Control-Allow-Credentials: true é inválido pelo spec. O browser rejeita a resposta mesmo que o servidor envie.

Preflight sem resposta 200/204 — Requisições com Content-Type: application/json ou headers customizados disparam um preflight OPTIONS. Se o servidor retornar 404 ou 405 para OPTIONS, o browser aborta antes de enviar a requisição real.

Header duplicado — Se o proxy (Nginx) e a aplicação (Express) ambos adicionam Access-Control-Allow-Origin, o browser recebe dois valores e rejeita. Escolha um lugar para adicionar.

Vary: Origin ausente — Quando o servidor reflete a origem dinamicamente, o Vary: Origin sinaliza para CDNs e proxies que a resposta varia por origem. Sem ele, a CDN pode entregar uma resposta cacheada com a origem errada para um cliente diferente.

Porta diferente conta como origem diferentehttp://localhost:3000 e http://localhost:5173 são origens distintas. Adicione ambas na allowlist durante o desenvolvimento.


Configurou o CORS mas ainda tem dúvidas sobre quais outros headers de segurança sua API está deixando de enviar? O scanner da VulnexusAI verifica CORS, HSTS, CSP e mais de 15 outros pontos em segundos.

Ler em InglêsLer em Espanhol

Perguntas frequentes

Por que Access-Control-Allow-Origin: * falha com cookies ou headers de Authorization?

O spec de CORS proíbe combinar uma origem wildcard com Access-Control-Allow-Credentials: true. Os navegadores rejeitam a resposta diretamente. Você precisa devolver uma origem específica da allowlist em vez de um wildcard sempre que houver credenciais envolvidas.

Por que preciso de Vary: Origin se já defino Access-Control-Allow-Origin dinamicamente?

Sem Vary: Origin, uma CDN ou proxy pode cachear a resposta gerada para uma origem e entregar essa mesma resposta cacheada para outra origem, causando falhas de CORS intermitentes e difíceis de reproduzir.

Por que minha requisição POST falha com erro de CORS embora o GET funcione?

Requisições com corpo JSON ou headers personalizados disparam primeiro uma requisição preflight OPTIONS. Se o seu servidor não tratar OPTIONS explicitamente e devolver um 200 ou 204, o navegador aborta antes de enviar a requisição POST real.

Leia também

Seu site passa nessas verificações?

Teste qualquer URL pública no scanner gratuito da VulnexusAI e veja um score de 0 a 100, com nota de A a F e dicas de correção.

Verificar meu site