ADR-012 — handleCorsPrelight exige checagem explícita de método pelo caller
- Status: Accepted
- Data: 2026-05-09
- Decisores: backend / segurança
- Contexto-pai: incidente CORS preflight bypass (
docs/security/INCIDENTS.md§2026-05-09)
Contexto
Antes de 2026-05-09, o helper supabase/functions/_shared/cors-helpers.ts:handleCorsPrelight() retornava Response(204) incondicionalmente. Duas funções (resend-webhook e email-unsubscribe) usavam o padrão:
const pre = handleCorsPrelight(req);
if (pre) return pre;Como handleCorsPrelight SEMPRE retorna Response, toda requisição (POST, PATCH, etc.) era curto-circuitada com 204 ANTES de qualquer validação de método/HMAC/auth.
Isso configurou um bypass real de validação HMAC no webhook Resend.
Decisão
Contrato do helper: o caller é responsável por checar
req.method === 'OPTIONS'. O helper retorna 204+CORS sempre — esse é o nome da função (handleCorsPrelight) e o tipo de retorno (Response). Não há "auto-detect".Padrão único de uso em toda a codebase:
tsif (req.method === 'OPTIONS') return handleCorsPrelight(req);Padrões alternativos como
const pre = …; if (pre) return pre;são PROIBIDOS.Guard de regressão automatizado:
_shared/__tests__/cors-helpers.test.tsfaz scan estático recursivo emsupabase/functions/e falha o CI se detectar o padrão proibido.
Alternativas consideradas
A) Helper retorna Response | null com checagem interna
export function handleCorsPrelight(req: Request): Response | null {
if (req.method !== 'OPTIONS') return null;
return new Response(null, { status: 204, headers: getCorsHeaders(req) });
}Rejeitada: quebra typing em ~38 callsites que retornam diretamente return handleCorsPrelight(req); dentro de if (req.method === 'OPTIONS'). Forçaria refactor massivo (return handleCorsPrelight(req)!;) sem ganho de segurança real (a segurança vem do scan estático, não da checagem interna).
B) Renomear para corsPreflightResponse
Postergada: nome mais explícito, mas exige refactor de 40 arquivos. Marcado como follow-up opcional em docs/security/INCIDENTS.md.
C) Dois helpers (com e sem guard interno)
Rejeitada: duplica API e introduz ambiguidade ("qual eu uso?"). O scan estático resolve sem custo de cognição.
Consequências
Positivas:
- Padrão único, fácil de revisar.
- Guard estático bloqueia regressão sem depender de revisão humana.
- Compatível com 38 callsites pré-existentes — zero refactor desnecessário.
Negativas / riscos remanescentes:
- Helper continua sendo "trapaceiramente útil" (sempre retorna Response). Mitigado por JSDoc + scan + ADR.
- Se alguém criar um novo padrão buggy não coberto pelo regex do scan, passa. Mitigação: o regex cobre o exato padrão observado; padrões novos exigem nova análise de incidente.
Referências
supabase/functions/_shared/cors-helpers.ts(implementação + JSDoc)supabase/functions/_shared/__tests__/cors-helpers.test.ts(guard)docs/security/INCIDENTS.md§2026-05-09 (incidente)mem://security/cors-preflight-method-guard(memory)