RealScan (Auditor Externo de Segurança)
Scanner de postura web em Cloudflare Workers: recebe um domínio e devolve um relatório com nota de A a F sobre HTTPS, headers de segurança, DNS de e-mail, subdomínios que podem expor a origem e segredos no código entregue ao navegador.
O problema
Erros de configuração em headers HTTP, registros DNS (SPF/DMARC) e vazamentos de credenciais no lado do cliente eram difíceis de auditar continuamente de fora para dentro.
A abordagem
Criei o RealScan, um scanner serverless em Cloudflare Workers que audita domínios de fora para dentro, checando headers, registros de e-mail e segredos expostos no cliente. A guarda anti-SSRF nega faixas privadas, loopback, link-local (incluindo o endpoint de metadados de nuvem), CGNAT e IPv4 escondido em IPv6.
Arquitetura
- 01Cliente → Worker (index.js): POST { url }
- 02Worker (index.js) → ScanLimiter DO: IP do cliente
- 03Worker (index.js) → ssrf.js: URL após o limite
- 04ssrf.js → DoH 1.1.1.1: resolve o host
- 05ssrf.js → scan.js: host validado
- 06scan.js → Site analisado: fetches limitados
- 07scan.js → DoH 1.1.1.1: SPF, DMARC, DNSSEC, CAA
- 08Worker (index.js) → Reports DO: waitUntil /save
Decisões
- 01
Regra de honestidade: só o que um Worker consegue verificar
Por quê Um Worker não consegue fazer um handshake TLS bruto contra um host arbitrário nem descobrir de verdade a origem atrás de uma CDN. Então fiz o RealScan não fingir que faz.
Custo Não há inspeção de certificado, e a detecção de subdomínios que expõem a origem é heurística e aparece rotulada como tal.
- 02
Guarda anti-SSRF que resolve o host antes do scan
Por quê Buscar URLs do usuário é um vetor de SSRF por padrão. O ssrf.js aceita só http/https nas portas 80/443, recusa IP literal e nomes internos, resolve A/AAAA e rejeita se algum endereço for privado ou reservado: RFC1918, loopback, link-local com 169.254.169.254, CGNAT, ULA, IPv4 mapeado em IPv6 e multicast.
Custo Alvos por IP ou em portas fora do padrão não podem ser analisados.
- 03
Rate limit antes de qualquer trabalho de saída
Por quê O scanner age em nome de quem chama, então o limite serve para impedir o uso do RealScan contra terceiros, não para UX. Roda antes do DoH e do scan; só uma checagem sintática vem antes, para um erro de digitação não ser mascarado pelo limite.
Custo Se o Durable Object do limite falhar, o scan segue sem limite (falha aberta).
- 04
Cada checagem isolada
Por quê Uma sonda que falha vira "não foi possível determinar" em vez de quebrar o relatório. O HTML do relatório escapa tudo o que vem do site analisado.
- 05
Leituras com teto de bytes e de scripts
Por quê Tratei esses valores como limites de memória e timeouts: o HTML é lido até 600000 bytes, cada script até 400000 bytes, e só os 6 primeiros scripts first-party são baixados para a busca de segredos. Segredos encontrados saem mascarados (4 primeiros e 4 últimos caracteres).
Custo Conteúdo além do teto, scripts de terceiros e scripts a partir do sétimo não são examinados.
Números com fonte
Cada número aponta para o arquivo do repositório de onde saiu.
- Scans por IP por janela
- 20 a cada 10 min (600000 ms) / 20 per 10 min (600000 ms)
- fonte: wrangler.toml, src/config.js
- Padrões de segredo
- 7 (AWS, Google, Stripe secret e publishable, Slack, GitHub, chave privada) / 7 (AWS, Google, Stripe secret and publishable, Slack, GitHub, private key)
- fonte: src/scan.js
- Custo por achado (alto / médio / baixo)
- 20 / 10 / 4, a partir de 100 / from 100
- fonte: wrangler.toml, src/scan.js
- Testes (Vitest)
- 32 passando / 32 passing
- fonte: tests/*.test.js (confirmado com vitest run / confirmed with vitest run)
Stack
- Cloudflare Workers
- JavaScript
- DNS-over-HTTPS