Segurança
Zip bomb: quando um arquivo de poucos KB derruba seu servidor
O que é um zip bomb, por que upload de arquivo sem limites de descompressão é um risco real, e como blindar APIs e workers que processam ZIP, GZIP e afins — sem precisar de banco nem de ferramenta fancy.
Imagine um endpoint que aceita upload de ZIP: backup, planilha compactada, anexo de integração, “importar dados”. O arquivo chega com 42 KB. O antivírus passa. O disco “tem espaço”. Você chama unzip (ou a lib equivalente) e, segundos depois, o processo come dezenas de gigabytes de RAM, o disco enche e o worker morre — ou pior: a máquina inteira fica sem memória.
Isso não é ficção. É o efeito clássico de um zip bomb (também chamado de decompression bomb): um arquivo compactado pequeno no disco, mas enorme quando expandido.
O que está acontecendo de verdade
Compressão existe porque dados reais têm redundância. Um ZIP “honesto” costuma expandir algo como 2×–10×. Um zip bomb explora o pior caso: conteúdo extremamente repetitivo (ou camadas aninhadas) que a algoritmo comprime de forma absurda.
O atacante não precisa “hackear” sua API. Ele só precisa que você confie no tamanho do arquivo compactado e descompacte sem limites.
Três ideias erradas que aparecem o tempo todo:
- “O limite de upload é 5 MB, então estou seguro.” — Você limitou o payload de rede, não o tamanho descomprimido.
- “É um arquivo pequeno, o disco aguenta.” — O problema muitas vezes é memória (buffer na RAM) antes do disco.
- “Só aceito ZIP de usuários autenticados.” — Conta comprometida, aluno curioso, bug de produto ou concorrente chato ainda passam pelo mesmo código.
Por que isso importa no backend
Qualquer lugar que abra um arquivo compactado é superfície:
- Upload de ZIP / GZIP / 7z / RAR
- Importação de CSV/XML “já zipado”
- Processamento de e-mail com anexo
- Pipelines que descompactam artefatos de CI
- Libs que leem “stream ZIP” em memória
O impacto típico:
| Efeito | O que você sente |
|---|---|
| OOM / swap | Processo morto, pods reiniciando, latência absurda |
| Disco cheio | Logs param, deploys quebram, banco pode travar se estiver no mesmo volume |
| CPU 100% | Descompressão é trabalho real — DoS “barato” para o atacante |
| Fila travada | Um job tóxico bloqueia workers honestos |
Não é exploit sofisticado de RCE. É negação de serviço por descompressão irresponsável.
Anatomia (alto nível, sem receita)
Sem entrar em “como fabricar uma bomba”, o padrão é:
- Razão de expansão absurda — poucos bytes no ZIP → muitos bytes fora.
- Às vezes camadas — ZIP dentro de ZIP (ou GZIP em cadeia). Cada camada parece inocente isolada; o produto explode.
- Muitos arquivos pequenos — não só “um arquivo de 100 GB”: milhares de entradas também esgotam inodes, handles e tempo de I/O.
Se o seu código faz “descompacta tudo para uma pasta temporária e depois processa”, você já está no caminho crítico.
Como se defender de verdade
Pense em limites em cada camada, não só no nginx.
1. Limite o tamanho descomprimido, não só o compactado
Antes (ou durante) a extração, defina um teto duro: por exemplo 10–50 MB descomprimidos no total, conforme o caso de uso. Se passar, aborte.
Muitas libs permitem ler entrada a entrada e contar bytes. Use isso. Se a lib só oferece “extract all”, troque a lib ou envolva com um sandbox.
2. Limite número de entradas e profundidade
- Máximo de arquivos dentro do arquivo (ex.: 100–500)
- Máximo de níveis de aninhamento (ideal: 0 — não descompacte ZIP-dentro-de-ZIP sem motivo)
- Nome de arquivo com path traversal (
../../etc/passwd) também precisa ser rejeitado — é outro clássico, mas costuma andar junto
3. Descompacte em streaming, com quota
Evite carregar o ZIP inteiro + conteúdo expandido na RAM. Leia entry → escreva em disco com contador de bytes → se estourar, delete e falhe.
4. Isole o trabalho
Descompactação em worker separado, com:
- limite de memória do processo/container
- timeout
- volume temporário com tamanho finito (
emptyDircomsizeLimit, tmpfs pequeno, etc.)
Assim um zip bomb mata o worker, não o processo da API que atende o resto do mundo.
5. Preferência: não aceite ZIP se não precisa
Se o produto só precisa de CSV/JSON/PDF, aceite o tipo final e pronto. Cada formato compactado que você “suporta por conveniência” é uma superfície nova.
Exemplo de mentalidade em código (TypeScript)
A ideia não é uma lib específica — é a política:
const MAX_COMPRESSED_BYTES = 5 * 1024 * 1024; // 5 MB no upload
const MAX_UNCOMPRESSED_BYTES = 30 * 1024 * 1024; // 30 MB no total expandido
const MAX_ENTRIES = 200;
// pseudofluxo:
// 1) rejeitar se size > MAX_COMPRESSED_BYTES
// 2) abrir o arquivo entry a entry
// 3) somar uncompressedSize (ou bytes realmente escritos)
// 4) se entries > MAX_ENTRIES ou total > MAX_UNCOMPRESSED_BYTES → throw
// 5) nunca confiar só no "declared size" do header sem validar o que foi escrito
Dois cuidados extras:
- Header mentiroso: alguns formatos declaram um tamanho; o stream real pode diferir. Conte o que saiu da descompressão.
- Ratio:
uncompressed / compressedabsurdo (ex.: > 100×) é um sinal de alerta — use como heurística, não como única defesa.
Checklist rápido antes de ir pra produção
- Limite de upload (gateway + app)
- Limite de bytes descomprimidos
- Limite de quantidade de arquivos
- Sem extração recursiva automática
- Path traversal bloqueado nos nomes
- Worker isolado + timeout + cota de disco/RAM
- Temp limpo sempre (sucesso ou falha)
- Log/métrica quando um upload estoura quota (útil pra ver abuso)
Conclusão
Zip bomb não é magia: é assimetria. O atacante manda pouco; o seu servidor trabalha muito. A defesa também é simples na ideia — nunca descompacte sem orçamento — e exige disciplina no código e na infra.
Se o seu produto tem “importar ZIP”, trate descompressão como operação perigosa: do mesmo jeito que você não executaria shell com input do usuário sem pensar, não deveria expandir arquivo arbitrário “até acabar”.