CVE-2026-23918 : Double-Free dans le module HTTP/2 d'Apache HTTPD
by cloud
CVE-2026-23918 est une vulnérabilité de type double-free dans le module HTTP/2 (mod_http2) d’Apache HTTP Server 2.4.66, identifiée par Bartlomiej Dmitruk (striga.ai) et Stanislaw Strzalkowski (isec.pl). Elle permet un déni de service (DoS) fiable et ouvre la voie à une potentielle exécution de code à distance (RCE).
Caractéristiques principales :
- ✅ DoS confirmé : ~800 streams HTTP/2 suffisent à tuer un worker
- ✅ RCE théoriquement possible (heap corruption contrôlée)
- ✅ Correctif disponible dans httpd 2.4.67
TL;DR
| Champ | Valeur |
|---|---|
| CVE | CVE-2026-23918 |
| Impact | Importante (Double-Free → RCE possible) |
| Version vulnérable | Apache HTTP Server 2.4.66 (mod_http2 < 2.0.37) |
| Version corrigée | 2.4.67 (mod_http2 2.0.37) |
| Composant | modules/http2/h2_mplx.c — gestion du tableau spurge |
| Type | Double-Free / Use-After-Free |
| Attaque | RST_STREAM HTTP/2 précoce → m_stream_cleanup() appelé 2× |
| Auteurs | Bartlomiej Dmitruk (striga.ai), Stanislaw Strzalkowski (isec.pl) |
| Correctif | Commit Apache |
1. Le Bug : Pourquoi double-free ?
1.1 Le tableau spurge
Le module HTTP/2 d’Apache gère les streams via un multiplexeur (h2_mplx). Quand un stream n’est plus utile, il est placé dans un tableau nommé spurge (contraction de streams to purge). La fonction c1_purge_streams() itère ce tableau et appelle h2_stream_destroy() sur chaque entrée, libérant la mémoire via apr_pool_destroy().
Jusque là, rien d’anormal. Le problème ? Aucune vérification de doublon lors de l’insertion dans spurge.
// h2_mplx.c (httpd 2.4.66)
static void m_stream_cleanup(h2_mplx *m, h2_stream *stream) {
if (c2_ctx) {
if (!stream_is_running(stream)) {
APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream; // ← sans vérif !
}
} else {
APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream; // ← idem
}
}
1.2 La callback chain qui tue
Ce qui rend ce bug exploitable, c’est la séquence d’appels lors de la réception d’un RST_STREAM. Tout se joue dans un seul appel à nghttp2_session_mem_recv() :
on_frame_recv_cbest appelé pourNGHTTP2_RST_STREAMon_stream_close_cbest appelé immédiatement après par nghttp2
Les deux callbacks invoquent h2_mplx_c1_client_rst() avec le même stream. Quand le stream arrive très tôt (avant d’être enregistré dans mplx->streams), le chemin !registered déclenche m_stream_cleanup() — deux fois.
Résultat : le même pointeur stream est poussé deux fois dans spurge.
// c1_purge_streams() — quand il traite le tableau spurge
for (i = 0; i < m->spurge->nelts; ++i) {
stream = APR_ARRAY_IDX(m->spurge, i, h2_stream*);
h2_stream_destroy(stream); // 1er appel : free() OK
h2_stream_destroy(stream); // 2e appel : FREE SUR MEMOIRE LIBEREE !
}
h2_stream_destroy() appelle apr_pool_destroy(stream->pool) qui libère le pool via free(). Le second appel est un double-free pur.
Ce schéma illustre le flux :
HEADERS → nghttp2 crée le stream (IDLE → OPEN)
RST_STREAM → nghttp2 appelle on_frame_recv_cb → h2_mplx_c1_client_rst() → m_stream_cleanup() → push spurge
→ nghttp2 appelle on_stream_close_cb → h2_mplx_c1_client_rst() → m_stream_cleanup() → push spurge (bis!)
→ spurge contient 2× le même pointeur
c1_purge_streams() → h2_stream_destroy(stream) → free()
→ h2_stream_destroy(stream) → free() → SIGSEGV
2. Démonstration DoS
2.1 Comment tuer un worker Apache
L’attaque est d’une simplicité déconcertante. Il suffit d’envoyer des paires HEADERS + RST_STREAM en rafale sur une connexion HTTP/2 :
- Le
HEADERScrée un stream - Le
RST_STREAMimmédiat est traité dans la même itération denghttp2_session_mem_recv() - Le stream n’a pas encore été enregistré → chemin
!registered→ double-add dansspurge
En pratique, environ 800 streams par connexion suffisent à déclencher un SIGSEGV sur le worker.
2.2 Dans les logs
[Thu May 07 09:29:18.229663 2026] [core:notice] [pid 1:tid 1] AH00052: child pid 167 exit signal Segmentation fault (11)
[Thu May 07 09:31:06.850207 2026] [core:notice] [pid 1:tid 1] AH00052: child pid 8 exit signal Segmentation fault (11)
[Thu May 07 09:31:06.851398 2026] [core:notice] [pid 1:tid 1] AH00052: child pid 13 exit signal Segmentation fault (11)
[Thu May 07 09:31:06.851420 2026] [mpm_event:warn] [pid 1:tid 1] AH10392: children are killed successively!
Le MPM event détecte la cascade et finit par loguer AH10392: children are killed successively!. À ce stade, le serveur est effectivement hors-service.
3. Analyse RCE
3.1 Nature du double-free
Le double-free porte sur un apr_pool_t. La fonction apr_pool_destroy() libère le pool via free(), ce qui corrompt les structures internes de l’allocateur (glibc malloc, jemalloc, etc.). C’est un scénario classique de heap corruption.
3.2 Fenêtre d’exploitation
Entre les deux appels à free() :
- Premier
free(): la mémoire retourne à l’allocateur - Fenêtre de vulnérabilité : possibilité d’allouer des données contrôlées à la même adresse
- Second
free(): corruption des métadonnées
Sous glibc avec tcache, un double-free immédiat est détecté par le safe-linking (glibc ≥ 2.32). Mais des allocations intermédiaires entre les deux free() pourraient permettre de contourner cette protection.
3.3 Vecteurs potentiels
| Vecteur | Description | Conditions |
|---|---|---|
| tcache poisoning | Double-free dans tcache → cycle dans la liste chaînée → arbitrary write | glibc < 2.32 ou bypass safe-linking |
| Fastbin corruption | Double-free dans fastbins (pas de safe-linking) | glibc < 2.34 (__malloc_hook) |
| Corruption APR | Corrompre la liste chaînée des cleanup handlers → détournement de flux | Connaissance de la structure APR |
3.4 Défis
Transformer ce DoS en RCE n’est pas trivial :
| Défi | Problème |
|---|---|
| Allocateur | Comportement différent entre glibc, jemalloc, musl |
| ASLR/PIE | Nécessite une fuite d’information préalable |
| Safe-linking | glibc ≥ 2.32 protège les tcache |
| __malloc_hook supprimé | glibc ≥ 2.34, plus de hook simple |
| Déterminisme | Le crash survient entre ~600-1000 streams (pas à un nombre fixe) |
| Structure APR | apr_pool_destroy est complexe, difficile à corrompre proprement |
3.5 Conclusion RCE
La RCE est théoriquement possible mais demande :
- Un heap feng shui précis pour placer des données contrôlées à l’adresse libérée
- Une fuite d’information pour contourner ASLR
- Un allocateur sensible (glibc pré-safe-linking, ou via contournement)
- Des primitives d’allocation/libération via d’autres trames HTTP/2 (SETTINGS, PRIORITY, HEADERS)
C’est faisable, mais l’effort d’ingénierie est conséquent. Un attaquant motivé pourrait y parvenir.
4. Le Correctif
Apache a réagi avec un correctif propre : introduction de add_for_purge() qui vérifie la présence du stream dans spurge avant d’ajouter.
// h2_mplx.c (httpd 2.4.67)
static int add_for_purge(h2_mplx *m, h2_stream *stream) {
int i;
for (i = 0; i < m->spurge->nelts; ++i) {
h2_stream *s = APR_ARRAY_IDX(m->spurge, i, h2_stream*);
if (s == stream)
return FALSE; /* déjà planifié pour purge */
}
APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream;
return TRUE;
}
Tous les APR_ARRAY_PUSH(m->spurge, ...) directs sont remplacés par add_for_purge(m, stream). Le module passe de la version 2.0.35 à 2.0.37.
5. Timeline de Disclosure
| Date | Événement |
|---|---|
| 2026-04-XX | Découverte par Bartlomiej Dmitruk et Stanislaw Strzalkowski |
| 2026-04-XX | Notification à l’équipe de sécurité Apache |
| 2026-05-06 | Publication de la version corrigée (httpd 2.4.67) |
| 2026-05-07 | Publication du CVE et divulgation publique |
6. Recommandations
Si vous utilisez Apache HTTP Server avec le module HTTP/2 activé :
- Mettez à jour immédiatement vers httpd ≥ 2.4.67
- Désactivez HTTP/2 temporairement si vous ne pouvez pas patcher :
Protocols h2c http/1.1 # ou retirez h2/h2c des directives Protocols - Surveillez les logs pour des SIGSEGV répétés (
AH00052: child pid X exit signal Segmentation fault) - Limitez le nombre de streams par connexion via
H2MaxSessionStreams(mesure défensive, ne résout pas le bug)
Références
| Source | URL |
|---|---|
| Apache security | https://httpd.apache.org/security/vulnerabilities_24.html |
| Commit de correctif | https://github.com/apache/httpd/commit/542e0da07048d3934ef18c22b44cf8d62e64067f |
| NIST NVD | CVE-2026-23918 (pas encore publié) |
Have fun.
tags: security - cve - apache - http2 - exploit