Wilker commited on
Commit ·
e42b704
1
Parent(s): 3352d77
docs: relatorio do incidente de seguranca em andamento
Browse files- SECURITY_INCIDENT_2026-07.md +94 -0
SECURITY_INCIDENT_2026-07.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Relatório — Ataque de Fuzzing no THC LLM Space (Julho/2026)
|
| 2 |
+
|
| 3 |
+
## Contexto
|
| 4 |
+
Servidor THC LLM (Hugging Face Space, https://hulktoigo-thcllm.hf.space) sob
|
| 5 |
+
ataque de fuzzing automatizado há cerca de 4 dias, em horários específicos do
|
| 6 |
+
dia. Padrão: scanner batendo em rotas conhecidas de vazamento de config
|
| 7 |
+
(.env, .git, actuator, streamlit, graphql, path traversal, etc).
|
| 8 |
+
|
| 9 |
+
## Evolução do ataque
|
| 10 |
+
1. Fase inicial: IP único martelando repetidamente as mesmas rotas.
|
| 11 |
+
2. Fase atual: rotação de IP (botnet/proxy) — cada IP gasta uma cota de
|
| 12 |
+
requisições, é banido, e o bot pula pro próximo IP. Isso sozinho gera
|
| 13 |
+
volume suficiente pra saturar o worker único do Space (free tier).
|
| 14 |
+
|
| 15 |
+
## Proteções implementadas em app.py (nesta ordem histórica)
|
| 16 |
+
|
| 17 |
+
### PROTEÇÃO 1 — já existia antes deste incidente
|
| 18 |
+
docs_url/redoc_url/openapi_url desativados em produção (só ativos com
|
| 19 |
+
THC_DEBUG=true), evitando que scanners mapeiem as rotas via /openapi.json.
|
| 20 |
+
|
| 21 |
+
### PROTEÇÃO 2 — já existia antes deste incidente
|
| 22 |
+
Middleware `block_known_fuzzing_patterns` com lista `_FUZZING_PATTERNS`
|
| 23 |
+
(.env, .git, .streamlit, actuator, graphql, path traversal codificado etc.)
|
| 24 |
+
bloqueando com 400 antes de consumir modelo/RAG.
|
| 25 |
+
|
| 26 |
+
### PROTEÇÃO 3 — adicionada neste incidente
|
| 27 |
+
- `get_real_ip(request)`: lê X-Forwarded-For antes de request.client.host,
|
| 28 |
+
porque HF Spaces expõe IPs internos do proxy (10.x.x.x / 10.16.x.x /
|
| 29 |
+
10.20.x.x) no client.host — não é o IP público real do atacante.
|
| 30 |
+
- Rate limiting em memória por IP real: janela deslizante de 60s, máximo 60
|
| 31 |
+
requisições, ban de 600s (10 min) ao estourar.
|
| 32 |
+
- `_is_authenticated_thc_key(request)`: chamadas com header X-THC-Key válido
|
| 33 |
+
(comparado contra THC_MASTER_CLI_KEY ou validado via verify_api_key do
|
| 34 |
+
módulo auth) pulam TODO o rate limit e ban — nunca é o atacante anônimo.
|
| 35 |
+
- Ban rápido específico para padrão de fuzzing: contador separado e mais
|
| 36 |
+
agressivo (`_fuzz_hits`), 5 hits em janela de 30s já bane o IP por 600s —
|
| 37 |
+
em vez de esperar o limite geral de 60 req/min. Isso corta o desperdício
|
| 38 |
+
de ~55 requisições por IP rotacionado. CONFIRMADO funcionando nos logs
|
| 39 |
+
(ban disparando em ~1s contra IPs novos).
|
| 40 |
+
|
| 41 |
+
### Correção de segurança relacionada (não é sobre o ataque em si)
|
| 42 |
+
THC_SESSION_SECRET (usada pelo SessionMiddleware pra assinar o cookie de
|
| 43 |
+
sessão do login OAuth) estava com fallback para string vazia — corrigido
|
| 44 |
+
para RuntimeError no boot se ausente. Causou outage curto no primeiro
|
| 45 |
+
deploy porque a secret ainda não estava cadastrada nos Secrets do Space;
|
| 46 |
+
resolvido gerando com `secrets.token_hex(32)` e cadastrando separada da
|
| 47 |
+
THC_MASTER_CLI_KEY (são segredos diferentes, propósitos diferentes — sessão
|
| 48 |
+
web vs autenticação de API).
|
| 49 |
+
|
| 50 |
+
## Estado atual (confirmado por teste real)
|
| 51 |
+
- Endpoint /v1/messages (formato Anthropic): funcionando perfeitamente,
|
| 52 |
+
autenticado, mesmo durante ataque ativo — resposta JSON completa testada.
|
| 53 |
+
- Endpoint /v1/messages/stream (SSE): funcionando perfeitamente, streaming
|
| 54 |
+
completo testado ponta a ponta (message_start → content_block_delta* →
|
| 55 |
+
message_stop), autenticado, durante ataque ativo.
|
| 56 |
+
- Bloqueio de fuzzing: confirmado ativo (400 em rotas conhecidas).
|
| 57 |
+
- Ban rápido: confirmado ativo, disparando em ~1s por IP rotacionado.
|
| 58 |
+
- Exceção X-THC-Key: confirmada ativa (chamadas autenticadas não recebem
|
| 59 |
+
429 mesmo com volume de ataque simultâneo).
|
| 60 |
+
|
| 61 |
+
## PENDÊNCIA ABERTA — não resolvida ainda
|
| 62 |
+
Teste em loop rápido (10 chamadas sequenciais, 1s de intervalo) contra
|
| 63 |
+
/v1/chat/completions (formato OpenAI) autenticado com X-THC-Key mostra
|
| 64 |
+
padrão intermitente: status 200 alternado com status 000 (conexão não
|
| 65 |
+
completa / timeout / reset — não é resposta HTTP nenhuma, então não é rate
|
| 66 |
+
limit nem fuzzing block, que sempre retornariam 400/429).
|
| 67 |
+
|
| 68 |
+
Hipóteses ainda não investigadas:
|
| 69 |
+
1. Worker único do Space (free tier, sem --workers) saturado processando
|
| 70 |
+
volume do ataque, mesmo com a exceção de rate limit já pulando a lógica
|
| 71 |
+
de contagem/ban para chamadas autenticadas — a saturação pode estar
|
| 72 |
+
acontecendo em nível de SO/rede antes mesmo do código da aplicação.
|
| 73 |
+
2. Throttling na camada de infraestrutura/CDN do próprio Hugging Face,
|
| 74 |
+
independente do código do app.py.
|
| 75 |
+
3. Comportamento do curl em loop com keep-alive de conexão.
|
| 76 |
+
|
| 77 |
+
Próximo passo de diagnóstico sugerido: rodar o mesmo teste em loop com
|
| 78 |
+
`curl -v` pra capturar o erro real da conexão (timeout vs reset vs DNS),
|
| 79 |
+
repetir em horário sem pico de ataque para comparar, e verificar se o
|
| 80 |
+
mesmo padrão se repete em /v1/messages sob a mesma carga (o teste anterior
|
| 81 |
+
foi interrompido cedo demais para confirmar).
|
| 82 |
+
|
| 83 |
+
## Decisão em aberto
|
| 84 |
+
Se a saturação persistir mesmo com as proteções de código, o próximo passo
|
| 85 |
+
sai do escopo de código e vai para: upgrade de hardware do Space (mais
|
| 86 |
+
CPU/workers) ou colocar uma camada de proteção externa (ex: Cloudflare
|
| 87 |
+
grátis) na frente do Space antes do tráfego chegar no container.
|
| 88 |
+
|
| 89 |
+
## Nota adicional (fora do escopo do ataque)
|
| 90 |
+
O endpoint /v1/messages não tem Depends(get_current_user) como os
|
| 91 |
+
endpoints de imagem/áudio têm — passa só pelo middleware de rate
|
| 92 |
+
limit/fuzzing, sem exigir autenticação formal. Verificar se isso é
|
| 93 |
+
intencional (compatibilidade com SDK) ou lacuna a fechar depois que o
|
| 94 |
+
ataque atual estiver sob controle.
|