--- pubDate = 2025-07-23T15:02:00 tags = ['http', 'web', 'nginx', 'zip-bomb', 'compression', 'sécurité'] lang = "fr" type = "note" [author] name = "ache" email = "ache@ache.one" [[alt_lang]] lang = "en" url = "/notes/html_zip_bomb" --- # Une bombe zip HTML valide ![Illustration d'une bombe zip](res/zip_bomb_file.svg) Beaucoup de sites ont été affectés par l'agressivité des web crowler destinés à améliorer des LLM. J'ai été relativement épargné, mais dès l'apparition du phénomène j'ai cherché une solution à mettre en place. Aujourd'hui, je présente donc [une bombe zip (gzip et brotli) qui constitue un HTML valide](/bomb.html). ## Les web crawler LLM Le problème initial est l'agressivité des web crawlers LLM qui ne respectent pas les `robots.txt`. La première idée qui vient à l'esprit est le blocage IP. Cependant, les web crawlers ont contourné cette restriction en utilisant des IP de particuliers via des botnets spécialisés. La solution qui vient donc à l'esprit est l'épuisement des ressources des moissoneurs. Avec une bombe zip, on tente d'épuiser leur mémoire vive.[^pro] On joue sur l'asymétrie des ressources nécessaires pour servir la bombe zip et eux pour là détecter. Forcément, je vais chercher à minimier au maximum les ressources nécessaires à la distribution de la bombe. ## Une bombe gzip La bombe gzip la plus basique est constituée de 0. ```shell $ dd if=/dev/zero bs=1M count=10240 | gzip -9 > 10G.gzip ``` Ce n'est pas mal : le ratio théorique est de 1032:1 (environ 1030 en pratique pour une bombe zip), donc notre fichier pèse environ 10 MiB. Le problème est que les navigateurs web parsent la page à la volée dès que possible et détectent assez rapidement qu'il ne s'agit pas d'une page HTML valide. Donc je me suis posé comme défi de créer une page valide HTML qui contient une bombe zip. ## La bombe zip HTML J'ai eu plusieurs idées. Déjà, puisque c'est une page HTML, on commence avec le doctype HTML5. Puis ensuite, on essaie de caser les 10 Mio de caractères identiques. J'ai d'abord tenté d'utiliser [les classes HTML qui peuvent contenir n'importe quoi](https://shkspr.mobi/blog/2025/05/decorative-text-within-html/), mais rapidement la solution du commentaire HTML m'a semblé la plus pratique. Alors j'ai mis en place un petit script shell (en [fish](https://fishshell.com/)) pour créer un fichier HTML avec un commentaire de 10 MiB de 'H'. ```bash #!/bin/fish # Base HTML echo -n 'Projet: Valid HTML bomb

This is a HTML valid bomb, cf. https://ache.one/articles/html_zip_bomb

" ``` Puis, on gzip tout ça: ```shell $ fish zip_bomb.fish | gzip -9 > bomb.html.gz $ du -sb bomb.html.gz 10180 bomb.html.gz ``` On a bien notre ratio de 1:1030, c'est parfait. ## Servir la bombe zip J'utilise nginx, l'idée est de servir le fichier pré-compressé. Idéalement, on ne veut même pas avoir le fichier de 10 Gio sur le serveur. Pour cela, on utilise le module `ngx_http_gzip_static_module` [^gzip_static_nginx]. ```nginxconf location = /bomb.html { gzip on; # Normalement ça devrait être le module gzip à la volé. 🤷 gzip_static on; gzip_proxied expired no-cache no-store private auth; gunzip off; # Surtout pas décompressé le gzip ! brotli_static on; # Mon site est disponible en brotli également alors pourquoi pas. } ``` Malheureusement, nginx renvoie une 404 si le fichier `bomb.html` n'existe pas, alors j'ai créé un petit fichier simple qui annonce que c'est une bombe gzip. ```shell $ curl https://ache.one/bomb.html You don't support gzip encoding. Add the HTTP header "accept-encoding: gzip". ``` Je vérifie que nginx sert correctement le fichier: ```shell $ curl -H "accept-encoding: gzip,br" -I -- https://ache.one/bomb.html | grep content content-type: text/html; charset=utf-8 content-length: 8298 content-encoding: br $ curl -H "accept-encoding: gzip" -I -- https://ache.one/bomb.html | grep content content-type: text/html; charset=utf-8 content-length: 10420650 content-encoding: gzip ``` Ok, la taille est bonne, maintenant il faut absolument s'assurer qu'on ne dépassera jamais le budget d'un web crawler légitime en l'interdisant dans le `robots.txt`. En le plaçant à la racine, je sais que mon `robots.txt` l'interdit déjà, mais sinon on devrait retrouver ceci : ```text User-agent: * Disallow: /bomb.html ``` ## Résultats Firefox rame beaucoup et finit par planter proprement avec une erreur `NS_ERROR_OUT_OF_MEMORY`, visible seulement dans les outils de développement. Si je mets la balise body avant le commentaire malicieux, j'aurais certainement une page correctement affichée. Chrome est bien plus rapide à planter ! Il offre un joyeux écran qui signale qu'une erreur s'est produite par un `SIGKILL`. Dans les deux cas, on remarque que la page est partiellement chargée cependant, le titre est correct. On est donc sûr et certain qu'un web crawler type Selenium va planter sur ce fichier HTML. Heureusement, il ne semble pas y avoir de faille de sécurité à exploiter. ## Évolution L'astuce des commentaires HTML n'est certainement pas la plus élégante. Je suis sûr qu'il y a plein d'idées qui permettent de caser des paquets de 258 caractères identiques[^max_paquet]. Cependant, ici, ça semble marcher si bien que je n'ai pas pris le temps d'explorer plus loin. L'intérêt d'avoir une bombe zip HTML plus variée serait de s'assurer que le parseur HTML n'optimise pas la lecture de certaines parties. D'ailleurs, je me suis permis de créer une version brotli également. En effet, mon site étant disponible en brotli et la bombe zip étant encore plus efficace en brotli, il n'y a pas de raison de ne pas le faire. [^pro]: Des solutions plus professionnelles ont vu le jour comme [anubis](https://anubis.techaro.lol/). [^gzip_static_nginx]: Vérifiez que votre version de nginx soit compilée avec ce module avec `nginx -V | grep gzip_static`. [^max_paquet]: Oui, j'ai oublié de le dire, 258 est le maximum qu'un fragment compressé puisse faire dans le format gzip, c'est une limite un peu arbitraire.