Contexte et principe de l'attaque

QPACK, le mécanisme de compression d’en‑têtes utilisé par HTTP/3, réduit la taille des champs en les indexant dans une table statique ou dynamique. Un champ de 71 octets comme strict-transport-security: max-age=31536000; includesubdomains; preload peut ainsi être transmis sous la forme d’un seul octet (0xFA). Le problème apparaît lorsque le serveur ne contrôle pas la taille du bloc d’en‑têtes après décodage. Selon la spécification RFC 9114 §4.2.2, chaque champ doit être comptabilisé comme len(name)+len(value)+32 octets. Un client non authentifié peut envoyer un HEADERS frame de 1 000 000 octets contenant environ 160 000 noms uniques, ce qui force le serveur à allouer plusieurs centaines de mégaoctets, dépassant largement le budget initial.

Mécanismes défaillants dans les implémentations

Sept des dix implémentations testées ne respectent pas le contrôle de taille décodée. Les erreurs récurrentes sont :

- Vérification de la taille du flux compressé plutôt que de la taille décodée.
- Décodage complet avant toute vérification.
- Omission de l’overhead de 32 octets par champ.
- Valeur par défaut illimitée lorsqu’aucun paramètre n’est configuré.
- Absence totale d’application du paramètre même lorsqu’il est défini.

Dans quic-go, la fonction

func DecodeFull(data []byte) []HeaderField { … }
décompresse l’ensemble du bloc en une passe et renvoie un tableau []HeaderField qui est ensuite converti en http.Header. Cette conversion crée une carte map[string][]string avec un surcoût mémoire supplémentaire, aboutissant à environ 15 MiB pour un bloc compressé de 1 MiB, soit un facteur 15 d’amplification.

Exemples d’impact sur les stacks HTTP/3

Le serveur quic-go, utilisé par Caddy, Traefik et cloudflared, limite le HEADERS frame à 1 MiB via DefaultMaxHeaderBytes, mais ce seuil s’applique au payload compressé. Le test montre que le même bloc décodé génère 160 075 champs et consomme ~15 MiB après le garbage collector, déclenchant rapidement un OOM.

Dans Cloudflare quiche, la fonction

fn decode(&mut self, ...) { size += name.len() + value.len(); }
ignore l’overhead de 32 octets, autorisant jusqu’à 33 fois plus de champs que prévu. Cette faille a conduit à la publication d’un CVE (non détaillé dans la source) et à la mise à jour corrective.

Le variant dynamique insère une entrée de ~4 KiB dans le flux d’encodeur, puis la référence des milliers de fois avec un octet d’index. Chaque référence entraîne la création d’une copie complète, créant un ratio d’amplification de 4064 : 1, exploité notamment contre les implémentations lsquic et Bun.

Correctifs et mesures d’atténuation

Les correctifs publiés avant le 20 octobre 2026 incluent :

- Envoi du paramètre SETTINGS_MAX_FIELD_SECTION_SIZE pour imposer une limite décodée.
- Décodage incrémental avec vérification après chaque champ.
- Retour d’une erreur HTTP 431 lorsque la limite est dépassée.

Les mitigations temporaires recommandées sont : ne pas exposer de serveurs HTTP/3 avec Bun, désactiver HTTP/3 dans mitmproxy (--set http3=false), et configurer max_field_section_size dans la crate h3. Les serveurs IIS, Envoy et les bibliothèques ngtcp2/nghttp3 ne sont pas affectés selon les tests.