Adriano Júnior
← Artigos

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:

EfeitoO que você sente
OOM / swapProcesso morto, pods reiniciando, latência absurda
Disco cheioLogs param, deploys quebram, banco pode travar se estiver no mesmo volume
CPU 100%Descompressão é trabalho real — DoS “barato” para o atacante
Fila travadaUm 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 é:

  1. Razão de expansão absurda — poucos bytes no ZIP → muitos bytes fora.
  2. Às vezes camadas — ZIP dentro de ZIP (ou GZIP em cadeia). Cada camada parece inocente isolada; o produto explode.
  3. 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 (emptyDir com sizeLimit, 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 / compressed absurdo (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”.