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:
// 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:
# 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:
// 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.brCORS no Fastify
Configuração equivalente para APIs Fastify:
// 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:
# 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
Conteúdos relacionados