Para configurar CORS em uma API Zig, trate o header Origin como entrada não confiável, compare-o com uma lista explícita de origens permitidas e devolva os headers CORS somente quando a política autorizar. Para requisições de preflight, responda a OPTIONS antes do handler principal e declare apenas os métodos e headers que a rota realmente aceita.
A regra mais importante é simples: não use Access-Control-Allow-Origin: * por conveniência em uma API autenticada. Se o frontend envia cookies ou outra credencial gerenciada pelo navegador, a resposta precisa indicar uma origem específica e pode usar Access-Control-Allow-Credentials: true. CORS não substitui autenticação, autorização, CSRF, validação de entrada nem rate limiting.
Como a API de std.http.Server ainda pode mudar antes do Zig 1.0, os exemplos abaixo concentram a lógica em funções pequenas e adaptáveis. Confira zig version e a documentação da versão instalada para ajustar a escrita dos headers e da resposta; a política HTTP permanece a mesma.
Resposta rápida: política recomendada
| Cenário | Allow-Origin | Credenciais | Observação |
|---|---|---|---|
| API pública sem cookies | * pode ser aceitável | não | ainda exige limites e autenticação quando aplicável |
| SPA própria em produção | origem exata, como https://app.exemplo.com.br | conforme o login | mantenha allowlist explícita |
| frontend local de desenvolvimento | http://localhost:3000 ou porta usada | conforme o login | não leve a liberação ampla para produção |
| vários domínios controlados | refletir somente origens presentes na allowlist | possível | envie Vary: Origin |
| origem desconhecida | não emitir headers CORS | não | não reflita o valor recebido |
Se você está começando o backend, leia primeiro o guia de API REST em Zig e o tutorial de servidor HTTP com std.http.Server. CORS deve entrar como uma política de borda pequena, não como lógica espalhada por cada handler.
O que CORS realmente faz
CORS (Cross-Origin Resource Sharing) é um protocolo entre o navegador e o servidor. Ele decide se JavaScript executado em uma origem pode ler a resposta de outra origem.
Uma origem é a combinação de:
esquema + host + porta
Portanto, estas origens são diferentes:
https://app.exemplo.com.br
http://app.exemplo.com.br
https://api.exemplo.com.br
https://app.exemplo.com.br:8443
O navegador pode até enviar a requisição em alguns casos, mas bloqueia o acesso da página à resposta quando os headers CORS não autorizam a leitura. Isso explica um erro comum: a API parece funcionar no curl e falhar no frontend. O curl não aplica a política do navegador.
CORS também não impede alguém de chamar sua API diretamente. Um bot pode copiar os mesmos headers, e um serviço backend pode ignorar completamente a política. A proteção real continua sendo composta por autenticação, autorização por recurso, validação, limites de body, rate limiting e logs. Para essa camada operacional, veja rate limiting com token bucket e configuração segura de segredos.
Requisição simples e preflight
O navegador não faz preflight para toda chamada. Algumas requisições consideradas “simples” podem ser enviadas diretamente, normalmente com métodos como GET, HEAD ou certos POST, desde que usem apenas headers e content types limitados pela especificação.
Quando o frontend tenta enviar algo como:
PUT /usuarios/42 HTTP/1.1
Origin: https://app.exemplo.com.br
Authorization: Bearer ...
Content-Type: application/json
o navegador tende a consultar a permissão primeiro:
OPTIONS /usuarios/42 HTTP/1.1
Origin: https://app.exemplo.com.br
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
O servidor deve responder sem executar a atualização:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.exemplo.com.br
Access-Control-Allow-Methods: GET, PUT, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
Vary: Origin
Somente depois dessa aprovação o navegador envia o PUT real. Se a rota OPTIONS cair no handler de autenticação, tentar ler JSON ou retornar 404, o frontend verá um erro de CORS antes que a operação principal aconteça.
Modele a política separadamente
Uma estrutura pequena evita condicionais duplicadas:
const CorsPolicy = struct {
allowed_origins: []const []const u8,
allowed_methods: []const u8 = "GET, POST, PUT, PATCH, DELETE, OPTIONS",
allowed_headers: []const u8 = "Authorization, Content-Type, Idempotency-Key",
exposed_headers: []const u8 = "ETag, X-Request-Id",
allow_credentials: bool = true,
max_age_seconds: u32 = 600,
};
A lista deve vir da configuração validada no startup, não de uma substring improvisada. Em produção, algo como CORS_ALLOWED_ORIGINS=https://app.exemplo.com.br,https://admin.exemplo.com.br pode ser convertido em uma lista e validado antes de o servidor aceitar tráfego.
Evite regras como:
origin termina com "exemplo.com.br"
A origem https://naoexemplo.com.br também termina com esse texto. Mesmo uma checagem de sufixo com ponto precisa considerar normalização, portas e domínios internacionais. Para a maioria das APIs, igualdade exata é mais fácil de revisar.
Validação explícita da origem em Zig
A função central pode ser independente do servidor HTTP:
const std = @import("std");
fn allowedOrigin(policy: CorsPolicy, origin: []const u8) ?[]const u8 {
for (policy.allowed_origins) |allowed| {
if (std.mem.eql(u8, origin, allowed)) return allowed;
}
return null;
}
test "aceita somente origem cadastrada" {
const origins = [_][]const u8{
"https://app.exemplo.com.br",
"http://localhost:3000",
};
const policy = CorsPolicy{ .allowed_origins = &origins };
try std.testing.expect(allowedOrigin(
policy,
"https://app.exemplo.com.br",
) != null);
try std.testing.expect(allowedOrigin(
policy,
"https://app.exemplo.com.br.atacante.test",
) == null);
}
Retornar o valor da allowlist, em vez de refletir diretamente o header recebido, reduz a chance de uma validação frouxa virar vulnerabilidade. Antes de comparar, imponha um limite pequeno ao tamanho de Origin; headers gigantes devem ser rejeitados na camada HTTP geral.
Aplicando headers à resposta
O adaptador concreto depende da versão de std.http.Server, mas a decisão pode produzir uma lista de headers que o handler adiciona à resposta:
const CorsHeaders = struct {
allow_origin: []const u8,
allow_methods: []const u8,
allow_headers: []const u8,
expose_headers: []const u8,
allow_credentials: bool,
max_age_seconds: u32,
};
fn evaluateCors(policy: CorsPolicy, origin: ?[]const u8) ?CorsHeaders {
const received = origin orelse return null;
const allowed = allowedOrigin(policy, received) orelse return null;
return .{
.allow_origin = allowed,
.allow_methods = policy.allowed_methods,
.allow_headers = policy.allowed_headers,
.expose_headers = policy.exposed_headers,
.allow_credentials = policy.allow_credentials,
.max_age_seconds = policy.max_age_seconds,
};
}
Na resposta real, traduza esse resultado para:
Access-Control-Allow-Origin: <origem aprovada>
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: ETag, X-Request-Id
Vary: Origin
Access-Control-Expose-Headers é necessário quando o JavaScript precisa ler headers que não fazem parte da lista exposta automaticamente pelo navegador. É comum liberar ETag, Location, X-Request-Id ou um header de paginação; não exponha informação interna sem necessidade.
Como tratar OPTIONS antes do roteamento
O fluxo recomendado é:
1. validar tamanho e sintaxe básica dos headers
2. ler Origin
3. identificar OPTIONS com Access-Control-Request-Method
4. avaliar a política CORS
5. responder 204 ou rejeitar
6. para a chamada real, executar autenticação e handler
7. adicionar headers CORS à resposta final quando a origem for permitida
Um esqueleto conceitual:
fn handleRequest(req: *Request, policy: CorsPolicy) !void {
const origin = req.header("origin");
const cors = evaluateCors(policy, origin);
if (req.method == .OPTIONS and
req.header("access-control-request-method") != null)
{
const approved = cors orelse {
return respondNoContent(req, .forbidden);
};
if (!requestedMethodAllowed(req, policy) or
!requestedHeadersAllowed(req, policy))
{
return respondNoContent(req, .forbidden);
}
return respondPreflight(req, approved);
}
// Autenticação, autorização, roteamento e handler normal.
const response = try route(req);
if (cors) |headers| addCorsHeaders(response, headers);
try response.send();
}
Os nomes Request, header, respondPreflight e send são deliberadamente adaptadores. O ponto importante é a ordem: o preflight não deve executar efeito colateral, e a resposta real também precisa incluir Access-Control-Allow-Origin. Aprovar somente OPTIONS não basta.
Valide método e headers solicitados
Não devolva uma lista ampla sem olhar o pedido. O preflight informa o método desejado em Access-Control-Request-Method e os headers em Access-Control-Request-Headers.
A validação deve:
- comparar método sem aceitar valores inventados;
- normalizar nomes de headers de forma case-insensitive;
- dividir a lista por vírgula;
- remover espaços externos;
- rejeitar header fora da allowlist;
- impor limite de quantidade e tamanho;
- não confundir header vazio com autorização irrestrita.
Se a API só aceita GET e POST, não anuncie DELETE. Se não usa X-Admin-Override, não o inclua em Access-Control-Allow-Headers. A resposta CORS também funciona como documentação de superfície da API.
Credenciais, cookies e CSRF
Access-Control-Allow-Credentials: true permite que o navegador disponibilize ao frontend uma resposta associada a credenciais, desde que a chamada do JavaScript também opte por enviá-las. Isso costuma aparecer com cookies de sessão:
fetch("https://api.exemplo.com.br/perfil", {
credentials: "include"
});
Nesse modo:
Access-Control-Allow-Originnão pode ser*;- a origem deve ser exata;
- o cookie deve ter atributos adequados, como
Secure,HttpOnlye políticaSameSitecoerente; - operações mutáveis ainda precisam de defesa contra CSRF quando a autenticação é automática por cookie;
Origine, quando apropriado,Refererpodem participar da defesa, mas não substituem token CSRF em todos os desenhos.
Bearer tokens enviados explicitamente em Authorization não tornam a API automaticamente segura. Um XSS no frontend pode acessar o token, e uma política CORS permissiva pode expor respostas a origens indevidas.
Por que Vary: Origin é obrigatório no caso dinâmico
Quando o servidor devolve uma origem diferente conforme a requisição, caches precisam saber que a resposta varia por Origin:
Vary: Origin
Sem isso, um CDN ou proxy pode armazenar uma resposta liberada para https://app.exemplo.com.br e entregá-la a outra origem. Se a resposta já possui Vary: Accept-Encoding, não substitua o valor; combine os campos corretamente.
O mesmo cuidado vale para cache de preflight. Access-Control-Max-Age: 600 reduz chamadas OPTIONS, mas também prolonga uma política antiga no navegador. Comece com alguns minutos, valide o comportamento e evite valores enormes durante migrações de domínio.
CORS no Nginx ou no serviço Zig?
As duas opções funcionam, mas escolha um único dono da política.
No serviço Zig, a política pode variar por rota e fica perto da autenticação. É mais fácil testar como código e evitar liberar métodos inexistentes.
No Nginx, Caddy ou gateway, a organização consegue padronizar vários serviços. Porém, configurações com condicionais e headers duplicados ficam difíceis de revisar. Um proxy que adiciona Allow-Origin: * por cima de uma origem específica pode gerar resposta inválida.
Para uma API pequena, manter a decisão no Zig e deixar o proxy apenas repassar Origin e OPTIONS costuma ser previsível. Em plataformas maiores, um gateway central pode ser adequado desde que haja testes de contrato. O guia de Zig por trás de Nginx cobre a fronteira entre proxy e aplicação.
Como testar com curl
Teste uma requisição real autorizada:
curl -i https://api.exemplo.com.br/perfil \
-H 'Origin: https://app.exemplo.com.br'
Teste o preflight:
curl -i -X OPTIONS https://api.exemplo.com.br/usuarios/42 \
-H 'Origin: https://app.exemplo.com.br' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: authorization, content-type'
Teste uma origem atacante:
curl -i -X OPTIONS https://api.exemplo.com.br/usuarios/42 \
-H 'Origin: https://app.exemplo.com.br.atacante.test' \
-H 'Access-Control-Request-Method: PUT'
A última resposta não deve refletir a origem. No teste automatizado, cubra ainda:
- origem permitida em
GET; - origem ausente, como chamada server-to-server;
- origem desconhecida;
- preflight com método permitido;
- preflight com método proibido;
- header permitido com capitalização diferente;
- header desconhecido;
- credenciais sem wildcard;
- presença de
Vary: Origin; - resposta de erro
401,403,404e500com CORS coerente.
Esse último item é importante: se apenas respostas 200 recebem CORS, o navegador esconde o body do erro e o frontend mostra “falha de CORS” no lugar da mensagem útil da API.
Erros comuns de configuração
Refletir qualquer Origin
Isto é praticamente equivalente a liberar todo mundo:
Access-Control-Allow-Origin: <valor recebido>
Só reflita após igualdade com uma allowlist confiável.
Usar wildcard com cookies
A combinação abaixo é inválida para credenciais:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Escolha uma origem específica ou remova credenciais.
Liberar somente no ambiente local
localhost:3000, localhost:5173 e 127.0.0.1:5173 são origens diferentes. Cadastre apenas as usadas pela equipe e mantenha a configuração de desenvolvimento separada da produção.
Exigir autenticação no preflight
O navegador não envia necessariamente as credenciais da operação final no OPTIONS. Avalie origem, método e headers no preflight; autentique a requisição real.
Tratar CORS como controle de acesso
Uma origem aprovada não prova quem é o usuário. O handler ainda precisa verificar se aquele usuário pode ler ou alterar o recurso solicitado.
Observabilidade sem vazar dados
Registre decisões CORS com baixa cardinalidade:
cors_preflight result=allowed route=/usuarios/:id method=PUT
cors_preflight result=denied reason=origin_not_allowed
cors_preflight result=denied reason=header_not_allowed
Evite colocar a origem completa como label livre de Prometheus, pois hosts aleatórios geram cardinalidade sem limite. Uma métrica útil pode separar apenas allowed, denied_origin, denied_method e denied_header. Para investigar, registre a origem normalizada em log estruturado com retenção e controles apropriados.
Checklist de produção
- Origens de produção usam igualdade exata com allowlist.
- Configuração de localhost não é carregada em produção.
- Preflight
OPTIONSé tratado antes do handler e sem efeito colateral. - Métodos anunciados correspondem às rotas reais.
- Headers solicitados são comparados de forma case-insensitive.
-
Access-Control-Allow-Originaparece também em respostas de erro aplicáveis. - Credenciais nunca são combinadas com origem
*. - Cookies têm
Secure,HttpOnlyeSameSiterevisados. - Operações autenticadas por cookie têm defesa contra CSRF.
- Respostas dinâmicas incluem
Vary: Origin. -
Access-Control-Max-Agetem duração moderada e revisável. - Proxy e aplicação não adicionam headers CORS conflitantes.
- Testes cobrem origem atacante parecida com o domínio legítimo.
- CORS não é tratado como substituto de autenticação ou autorização.
Perguntas frequentes
Como liberar CORS em uma API Zig?
Valide Origin contra uma lista explícita, devolva Access-Control-Allow-Origin apenas para origens autorizadas e responda ao preflight OPTIONS com os métodos e headers realmente aceitos. Não basta adicionar um asterisco a todas as respostas.
Posso usar Access-Control-Allow-Origin com asterisco e credenciais?
Não. Quando cookies ou credenciais do navegador estão habilitados, Access-Control-Allow-Origin precisa conter uma origem específica. O valor * não pode ser combinado com Access-Control-Allow-Credentials: true.
Por que o navegador envia OPTIONS antes do POST ou PUT?
Essa requisição é o preflight. O navegador verifica se a origem, o método e os headers pretendidos são aceitos antes de enviar uma operação que não se enquadra como requisição simples.
CORS protege a API contra curl, bots ou chamadas entre servidores?
Não. CORS é aplicado pelo navegador. curl, scripts e backends continuam capazes de chamar o endpoint, portanto autenticação, autorização, rate limiting e validação permanecem obrigatórios.
Como testar CORS sem depender de um frontend?
Use curl enviando Origin e, no preflight, Access-Control-Request-Method e Access-Control-Request-Headers. Teste origens permitidas e rejeitadas, verifique Vary: Origin e confirme que respostas de erro também seguem a política.
Conclusão
Uma implementação segura de CORS em Zig não precisa virar um framework. Ela pode ser composta por três partes pequenas: uma allowlist validada no startup, uma função pura que decide se a origem é aceita e um adaptador HTTP que trata preflight e adiciona headers à resposta final.
O principal é manter o contrato explícito. Origem não é identidade, preflight não executa a operação, wildcard não combina com credenciais e CORS não bloqueia clientes fora do navegador. Com essas fronteiras claras, o frontend consegue conversar com a API sem transformar uma conveniência de desenvolvimento em uma liberação acidental de produção.
Para completar a camada HTTP, conecte esta política ao guia de autenticação JWT em Zig, aos limites e health checks do servidor em produção e ao checklist de TLS e certificados.