CORS em Node.js: configuração segura

    CORS (Cross-Origin Resource Sharing) é o mecanismo que permite — ou bloqueia — que um frontend em domínio diferente consuma sua API. Configurar Access-Control-Allow-Origin como "*" é a forma mais rápida de criar uma API, mas também a mais perigosa. Configuração correta com whitelist de origens protege contra ataques CSRF cross-origin.

    CORS com whitelist de origens no Express

    Configuração segura com lista de origens permitidas:

    typescript
    // npm install cors @types/cors
    
    import cors from 'cors'
    import type { CorsOptions } from 'cors'
    
    const ORIGENS_PERMITIDAS = [
      'https://seudominio.com.br',
      'https://www.seudominio.com.br',
      'https://app.seudominio.com.br',
      // Desenvolvimento:
      ...(process.env.NODE_ENV === 'development'
        ? ['http://localhost:3000', 'http://localhost:5173']
        : []),
    ]
    
    const corsOptions: CorsOptions = {
      origin(origin, callback) {
        // Permitir requests sem origin (curl, Postman, server-to-server):
        if (!origin) return callback(null, true)
    
        if (ORIGENS_PERMITIDAS.includes(origin)) {
          callback(null, true)
        } else {
          callback(new Error(`Origem não permitida pelo CORS: ${origin}`))
        }
      },
      credentials: true,          // permitir cookies e headers de auth
      methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
      allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Org-Id'],
      exposedHeaders: ['X-RateLimit-Limit', 'X-RateLimit-Remaining'],
      maxAge: 86400,              // cache do preflight por 24h (reduz OPTIONS requests)
    }
    
    app.use(cors(corsOptions))
    
    // Para rotas específicas com CORS diferente:
    app.get('/api/public/embed', cors({ origin: '*' }), embedHandler)

    Preflight requests e OPTIONS

    Como o browser verifica o CORS antes de requisições complexas:

    bash
    # O browser envia um preflight OPTIONS antes de:
    # - Métodos não-simples (PUT, DELETE, PATCH)
    # - Headers customizados (Authorization, X-API-Key)
    # - Content-Type != text/plain ou multipart/form-data
    
    # Exemplo de preflight:
    # OPTIONS /api/pedidos HTTP/1.1
    # Origin: https://seudominio.com.br
    # Access-Control-Request-Method: POST
    # Access-Control-Request-Headers: Content-Type, Authorization
    
    # Resposta esperada do servidor:
    # HTTP/1.1 204 No Content
    # Access-Control-Allow-Origin: https://seudominio.com.br
    # Access-Control-Allow-Methods: GET, POST, PUT, DELETE
    # Access-Control-Allow-Headers: Content-Type, Authorization
    # Access-Control-Max-Age: 86400
    # Vary: Origin
    
    # Garantir que o Express responde ao OPTIONS antes de outros middlewares:
    app.options('*', cors(corsOptions))  // habilitar preflight para todas as rotas
    
    # Para Nginx — adicionar headers CORS no proxy (alternativa ao Express):
    # /etc/nginx/conf.d/api.conf:
    server {
        location /api/ {
            add_header Access-Control-Allow-Origin $http_origin always;
            add_header Access-Control-Allow-Credentials true always;
            add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
            add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
    
            if ($request_method = OPTIONS) {
                add_header Access-Control-Max-Age 86400;
                return 204;
            }
            proxy_pass http://127.0.0.1:3000;
        }
    }

    CORS com subdomínios dinâmicos

    Permitir qualquer subdomínio do seu domínio principal:

    typescript
    // Para multi-tenant onde cada cliente tem seu subdomínio:
    // cliente1.seuapp.com.br, cliente2.seuapp.com.br, ...
    
    const corsOptions: CorsOptions = {
      origin(origin, callback) {
        if (!origin) return callback(null, true)
    
        // Regex para subdomínios do domínio principal:
        const dominioBase = /^https:\/\/([a-z0-9-]+\.)?seuapp\.com\.br$/
    
        if (dominioBase.test(origin)) {
          callback(null, true)
        } else if (process.env.NODE_ENV === 'development' && origin.startsWith('http://localhost:')) {
          callback(null, true)
        } else {
          callback(new Error('Origem não permitida'))
        }
      },
      credentials: true,
    }
    
    // Para APIs públicas com autenticação por API Key:
    // Não precisa de CORS restritivo — a API Key já autentica
    // Mas NUNCA use credentials: true com origin: '*'
    // (browsers não suportam essa combinação por razões de segurança)
    
    // Verificar a configuração atual em produção:
    curl -I -H "Origin: https://seudominio.com.br" \
         -H "Access-Control-Request-Method: POST" \
         -X OPTIONS \
         https://api.seudominio.com.br/api/pedidos
    # Deve retornar: Access-Control-Allow-Origin: https://seudominio.com.br

    CORS no Fastify

    Configuração equivalente para APIs Fastify:

    typescript
    // npm install @fastify/cors
    
    import Fastify from 'fastify'
    import cors from '@fastify/cors'
    
    const app = Fastify({ logger: true })
    
    await app.register(cors, {
      origin: (origin, cb) => {
        if (!origin) return cb(null, true)
    
        const permitidas = ['https://seudominio.com.br', 'https://app.seudominio.com.br']
        if (permitidas.includes(origin)) {
          cb(null, true)
        } else {
          cb(new Error('Origem não permitida'), false)
        }
      },
      credentials: true,
      methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
      allowedHeaders: ['Content-Type', 'Authorization'],
      maxAge: 86400,
      preflight: true,
      strictPreflight: true,  // rejeitar preflights com headers não declarados
    })
    
    // Sobrescrever CORS para rota específica:
    app.get('/embed', {
      config: {
        cors: { origin: '*', credentials: false },
      },
    }, async (req, reply) => {
      return { embeddable: true }
    })

    Erros comuns de CORS e como resolver

    Diagnóstico dos erros mais frequentes:

    bash
    # Erro: "has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header"
    # Causa: servidor não retorna o header CORS
    # Solução: verificar se o middleware cors está ANTES das rotas
    # Errado:
    app.get('/api/dados', handler)
    app.use(cors())  # ← cors depois das rotas não funciona!
    # Correto:
    app.use(cors())  # ← primeiro
    app.get('/api/dados', handler)
    
    # Erro: "has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin'
    #        header must not be the wildcard '*' when credentials mode is 'include'"
    # Causa: credentials: true com origin: '*'
    # Solução: especificar a origem exata:
    # credentials: true + origin: 'https://seudominio.com.br'  ✅
    # credentials: true + origin: '*'  ❌ (inválido por spec)
    
    # Erro: "CORS preflight response did not succeed"
    # Causa: servidor retorna 4xx/5xx no OPTIONS
    # Solução: adicionar app.options('*', cors()) ANTES de middlewares de auth:
    app.options('*', cors(corsOptions))  # antes de qualquer auth middleware
    
    # Erro: "Response to preflight request doesn't pass access control check:
    #        It does not have HTTP ok status."
    # Causa: middleware de auth bloqueando o OPTIONS
    # Solução: pular auth para OPTIONS:
    app.use((req, res, next) => {
      if (req.method === 'OPTIONS') return next()
      return autenticarMiddleware(req, res, next)
    })

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes