# CVE-2026-124040 — CivetWeb HTTP/2 HPACK header poisoning leads to conditional RCE
**NVD:**
**CVSS:** 8.1 (High) `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H` · 9.2 (Critical) `CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N`
A use-after-free in CivetWeb's HTTP/2 request handling lets an unauthenticated
remote client corrupt HPACK dynamic-table header names. A stale `request_uri`
pointer is dereferenced after the request header list has been freed; if the
freed allocation has been reused for a live HPACK header-name copy containing
`?`, `handle_request()` performs a one-byte NUL write that truncates the name.
A name registered as `:path?x`, `:method?x`, or `x-admin?x` therefore becomes
`:path`, `:method`, or `x-admin`. A later HEADERS frame containing only indexed
references is interpreted as an attacker-chosen request such as
`PUT /admin/exec` or `x-admin: 1`, while a frame-forwarding front-end only sees
benign literal headers and does not block the request.
When the backend application trusts the effective method/path or a
front-end-injected security header, this results in pre-authentication command
execution. Stock CivetWeb does not ship such a privileged callback, so the RCE
is deployment-dependent; the underlying primitive also bypasses literal-header
| 588860e3 (master snapshot 2026-04-19) | Vulnerable code present |
| v1.13–v1.15 | HTTP/2 stale-state crash reproduced; RCE chain not verified |
| v1.12 and earlier | Relevant HTTP/2 path absent |
`src/http2.inl` retains HPACK-decoded string pointers in request state
(`request_uri`, `local_uri`, `local_uri_raw`, `request_method`) and calls
`free_buffered_request_header_list()` without invalidating them. A subsequent
HEADERS frame can reuse those dangling pointers.
The write occurs in `handle_request()` in `src/civetweb.c`:
if ((conn->request_info.query_string = strchr(ri->request_uri, '?')) != NULL) {
*((char *)conn->request_info.query_string++) = '\0';
In the demonstrated glibc tcache layout, the freed `:path` allocation is reused
for an HPACK dynamic-table entry name. The resulting NUL write turns
`:path?x` into `:path`, so an indexed reference such as `0xBE 0xBF` supplies
`:method PUT` and `:path /admin/exec` without their literal names appearing in
The demonstrated command execution requires all of the following:
1. A CivetWeb build with TLS/ALPN and the HTTP/2 processing path enabled.
2. A front-end that inspects literal header representations but forwards
3. A backend callback that performs a privileged action based on the effective
method/path or a trusted front-end security header.
4. Request and allocator state that permit the stale-pointer reuse and HPACK
CIVETWEB_DIR=/path/to/vulnerable/civetweb ./verify.sh
CIVETWEB_DIR=/path/to/vulnerable/civetweb MODE=header ./verify.sh
[PASS] hidden PUT /admin/exec bypassed the literal-header ACL; callback executed id
[PASS] hidden x-admin header bypassed the literal-header ACL; callback executed id
`admin_server.c` is a deliberately privileged demonstration application. It
runs `id` when it receives `PUT /admin/exec` or `x-admin: 1`; this callback is
not part of CivetWeb. `proxy.py` is a frame-forwarding front-end that blocks
the literal values but passes indexed references unchanged.
CIVETWEB_DIR=/path/to/patched/civetweb EXPECT_SAFE=1 ./verify.sh
CIVETWEB_DIR=/path/to/patched/civetweb EXPECT_SAFE=1 MODE=header ./verify.sh
- resets per-request state when a new HTTP/2 HEADERS frame starts a request,
- clears `local_uri_raw` in `reset_per_request_attributes()`,
- rejects HTTP/2 requests missing mandatory `:method` / `:path` pseudo-headers.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
