Apache httpd 2.4.71 Fixes Critical HTTP/2 RCE (CVE-2026-119618): Root Cause, Detection and Patch Guide

The CyberSec Guru

Apache 2.4.71 Fixes Critical HTTP_2 RCE (CVE-2026-119618)

If you like this post, then please share it:

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Why your support matters: Zero paywalls: Keep the main content 100% free for learners worldwide.

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

Buy Me a Coffee Button

Apache HTTP Server 2.4.71 came out on November 11 with three security fixes. The most important is a single changed line in modules/http2/h2_request.c, inside assign_headers(). A worker thread received a request structure it was supposed to treat as read-only, and it wrote a synthesized Content-Length: 0 header into a table it did not own. The Apache project rates the result as critical: heap corruption reachable by any unauthenticated client that can speak HTTP/2 to an affected server, with a path to remote code execution. The release arrived five days after 2.4.70, which says a lot about how the project triaged it.

How many servers are exposed to the Apache HTTP/2 vulnerability?

As of November 12, W3Techs measures Apache on 20.9% of all websites whose web server it can identify. That puts it behind Cloudflare Server and nginx and ahead of LiteSpeed. The count leaves out the reverse proxies, ingress gateways, and internal application front ends that never show up in public surveys.

Wiz Research scans cloud estates instead of public websites. It found that 64% of the cloud environments it observes contain resources running vulnerable versions, and 51% have publicly exposed instances. Wiz notes that its figure ignores configuration, and configuration decides whether this bug matters. A server that never negotiates HTTP/2 never runs the vulnerable code.

What Apache fixed in 2.4.71

The official announcement calls 2.4.71 the latest GA release and “the best version of Apache available,” wording the project reserves for releases it wants everyone to install. The security advisory lists three CVEs:

  • CVE-2026-119618 (critical): a race condition in mod_http2. It affects httpd 2.4.55 through 2.4.70 and standalone mod_h2 builds from GitHub in the range 2.0.8 through 2.0.42.
  • CVE-2026-106023 (important): a NULL pointer dereference in mod_md during HTTP request processing. It affects every release from 2.4.30 through 2.4.70, which reaches back to 2018.
  • CVE-2026-109163 (low): a one-byte stack buffer overwrite in mod_vhost_alias that survived the earlier fix for CVE-2026-63292. It affects only 2.4.69 and 2.4.70.

Who reported the vulnerabilities

The credit lines read like a census of modern vulnerability research. Anthropic’s OSS Scanner found the HTTP/2 flaw as ANT-2026-MAPKT6MZ and reported it on October 26. Anthony Weems and Simon Scannell of Google Cloud Vulnerability Research reported the same bug independently on November 3. The fix, commit 824beab9b9424d6cfb29595248fdbf6c3dddeab1, landed on November 11, sixteen days after the first report.

The mod_md flaw was reported on September 29 by hexb1n, Yang Song, and JiaSheng He of the 360 Vulnerability Research Institute working with 360 TuLongFeng. Lucian Nitescu and Bartlomiej Dmitruk at striga.ai reported it separately. The mod_vhost_alias residual drew five finders: Gustavo Aiello, Charles Vosburgh, Wojciech Tatarski of CloudLinux/TuxCare, Oz Kaiser, and Daisuke Hatakeyama of SYZD Research. The first report came on October 5, four days after the incomplete fix shipped in 2.4.69.

CVSS scores and exploitation status for CVE-2026-119618

Scoring is still settling. NVD had not assigned a CVSS vector at the time of writing. Red Hat and Tenable both carry 9.8 with the vector AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, which means network reachable, low complexity, no privileges, no user interaction, and full compromise of confidentiality, integrity, and availability.

The sources disagree on exploitation. Wiz and Feedly report no public proof-of-concept and no in-the-wild activity as of publication. Red Hat’s advisory says that “public research has demonstrated a reliable exploit against a default-configured server with the mod_http2 module enabled.” Whether or not a weaponized exploit is circulating, the vendor shipping the fix to enterprise Linux describes the bug as reliably exploitable. Treat it that way.

How the mod_http2 race condition works

To see how one line goes wrong, you have to follow an HTTP/2 request across Apache’s threading model. Under a threaded MPM such as mpm_event, mod_http2 splits connection handling from request execution.

A session thread owns the TLS socket and drives the nghttp2 state machine, parsing binary frames and decompressing HPACK header blocks. As headers arrive, h2_request_add_header() fills an h2_request structure. Its headers member is an APR table, req->headers, allocated from memory tied to the stream and connection lifecycle. When the header block completes, h2_request_end_headers() finalizes authority handling and the stream is scheduled onto a pool of worker threads.

📬 Stay Ahead of Cyber Threats

Get the latest cybersecurity news, critical vulnerabilities, threat intelligence, tutorials, and exclusive giveaways delivered straight to your inbox. No spam. Unsubscribe anytime.

A worker picks up the stream and calls h2_create_request_rec(), which builds the request_rec that the rest of Apache understands. Every downstream consumer, from authentication hooks to mod_proxy to CGI and FastCGI connectors, reads incoming headers from r->headers_in. The translation happens in assign_headers(request_rec *r, const h2_request *req, int no_body, int is_connect).

Look at the const on req. It is an ownership contract: the worker may read the HTTP/2 request metadata, but the structure belongs to the session side. The function’s first move honors that contract by cloning the shared table into the worker’s own request pool:

c

r->headers_in = apr_table_clone(r->pool, req->headers);

From that point on, the worker should only modify its private copy. In one place, it doesn’t.

The one-line bug in assign_headers()

HTTP/2 does not need Content-Length to frame a body. The protocol carries explicit lengths and signals the end of a payload with the END_STREAM flag. Apache’s internal pipeline grew up on HTTP/1.1 semantics, though, and some handlers misbehave when a request advertises a Content-Type with no length indicator. So mod_http2 synthesizes one.

When a request has no body (END_STREAM arrived on the HEADERS frame), no Content-Length, and a Content-Type, the guards in assign_headers() add Content-Length: 0. In every release from 2.4.55 through 2.4.70, the code looked like this:

c

if (!is_connect) {
cl = apr_table_get(req->headers, "Content-Length");
if (no_body) {
if (!cl && apr_table_get(req->headers, "Content-Type")) {
/* If we have a content-type, but already seen eos, no more
* data will come. Signal a zero content length explicitly. */
apr_table_setn(req->headers, "Content-Length", "0"); /* BUG */
}
}
...
}

The target is req->headers, the shared table, instead of r->headers_in, the private clone. The C type system does not catch this. const h2_request *req makes the members of req read-only through that pointer, but req->headers is a pointer to a non-const apr_table_t. Reading that pointer and passing it to apr_table_setn(apr_table_t *, ...) is perfectly legal. The const protects the struct field, not the table on the other end of the pointer. The compiler emits no warning, and the write goes into memory the worker does not own.

The fix redirects the write to the clone:

diff

- apr_table_setn(req->headers, "Content-Length", "0");
+ apr_table_setn(r->headers_in, "Content-Length", "0");

After this change the shared table is never touched once it has been created, and the worker modifies only memory in its own r->pool. The danger was never the header itself. It was where the header got written.

How a stray header write becomes heap corruption

Writing to the wrong table sounds like a logic bug, not a memory-safety bug. It escalates because of how APR tables and pools are built.

An apr_table_t is a dynamic array of {key, val, key_checksum} entries wrapped in an apr_array_header_t, plus a lookup accelerator made of an index_initialized bitmask and index_first/index_last arrays keyed by a hash of the header name. It is not a thread-safe container. apr_table_setn() computes a checksum, consults the index, and either overwrites an existing entry (compacting duplicates by shifting array elements and reindexing) or appends a new one.

The vulnerable path only fires when Content-Length is absent, so it takes the append branch, which calls apr_array_push. If the array is full, apr_array_push doubles it. It allocates a new block from the table’s pool with apr_palloc, copies the old entries, and repoints the array.

That allocation is the problem. APR pools are bump allocators, and without an explicit mutex they are designed for single-threaded use. Red Hat’s root-cause description puts it bluntly: “a worker thread writes into memory owned by the connection thread through a shared, lock-free allocator.”

When the worker’s apr_table_setn forces a reallocation from the connection-owned pool at the same moment the session thread allocates from that same allocator to parse the next frame, two threads update the allocator’s free-pointer and node bookkeeping without synchronization. The result can be two overlapping allocations, meaning two live objects that share the same bytes.

From there the route to code execution is a familiar one. Request structures, filter chains, bucket brigades, and pool cleanup callbacks (which store function pointers invoked when a pool is destroyed) all live in that heap. An attacker who can shape the overlapping data and win the race can redirect a control-flow pointer and run code as the httpd child user, typically www-data, apache, or daemon.

Why severity differs between vendors

The allocator has a documented history that explains why vendors rate this CVE differently. The standalone mod_h2 project gave each stream its own memory allocator in v2.0.33, reverted that in v2.0.36, restored it in v2.0.37, and removed it again in v2.0.39 “after reports of memory problems with third party modules.” httpd picked up that last change in 2.4.67.

With per-stream allocators, a worker’s reallocation stays in memory the connection thread is not touching. With a shared allocator, it doesn’t. Red Hat states that its RHEL 9 and 10 mod_http2 packages allocate each HTTP/2 stream from a separate allocator, which “substantially increases the complexity of exploiting the flaw.” It rates those products Important instead of Critical. RHEL 8 and earlier are not affected at all, because their mod_h2 predates 2.0.8 and lacks the code.

The same CVE can carry different real-world risk depending on how a distribution built the module, so a version string alone won’t tell you which case you’re in.

How to test for the vulnerability without exploiting it

The ordering of two statements in the vulnerable function produces a side effect that is observable even when no race occurs. The clone into r->headers_in happens first, and the write of Content-Length: 0 into req->headers happens second. On an unpatched server, the synthesized header never reaches the private table that the handler actually reads.

Send an HTTP/2 request whose HEADERS frame carries both END_STREAM and END_HEADERS, includes a Content-Type (say text/plain on a POST), and omits Content-Length. On a vulnerable build, the downstream CGI or FastCGI environment has no CONTENT_LENGTH at all. On 2.4.71, the write lands in r->headers_in before dispatch, and the handler sees CONTENT_LENGTH=0.

Point that request at any endpoint that reflects the value back, such as a CGI script, a PHP page, or a proxied backend that echoes headers. You now have a deterministic, non-destructive check for whether commit 824beab9 is present. It attempts no heap corruption and needs no race. A community scanner built on this check appeared on GitHub within hours of disclosure and is listed among Tenable’s references for the CVE.

For defenders, this is a clean way to verify patch coverage across a fleet, especially where package managers backport fixes and version banners lie. Attackers get the same oracle as a cheap reconnaissance primitive, since they can map which of your public endpoints are vulnerable before they develop timing-sensitive exploit traffic. The gap between disclosure and targeted scanning is now measured in hours.

CVE-2026-106023: mod_md crashes on a request with no hostname

This flaw is less glamorous than the HTTP/2 bug and, in some estates, more broadly reachable, because the affected range starts at 2.4.30. mod_md automates certificate issuance and renewal over ACME (RFC 8555). To answer HTTP-01 domain-validation challenges, it hooks directly into request processing.

Two of those hooks are the problem. md_http_challenge_pr() intercepts requests under /.well-known/acme-challenge/, and md_http_cert_status() serves staged-certificate status. Both look up the managed domain for the request by calling md_get_by_domain(sc->mc->mds, r->hostname).

The fix commit shows what was missing. It adds an early return NULL in md_get_by_domain() when the domain argument is NULL. It also adds if (!r->hostname) return DECLINED; guards in both hooks, with a comment explaining the trigger: “No Host: (e.g. an HTTP/1.0 request without one) means no domain to look up; leave the request to normal handling.”

An HTTP/1.0 request that omits the Host header and targets an ACME challenge path or the certificate-status endpoint leaves r->hostname NULL. The unpatched code passes that NULL into a domain comparison that dereferences it.

On Linux and other POSIX systems the dereference raises SIGSEGV. Threads in a process share one address space, so the signal kills the entire httpd child, not just the offending request. Under mpm_event, a child multiplexes dozens of worker threads (ThreadsPerChild defaults to 25) and all the keep-alive and HTTP/2 connections they hold, so one cheap request drops every in-flight exchange on that child.

The parent process notices via waitpid(), logs the segmentation fault, and forks a replacement. httpd throttles child respawns, though, and backs off when children die at startup. An attacker who paces crash-triggering requests to the respawn rate can keep the worker pool exhausted while spending almost no bandwidth. This is a denial-of-service flaw with no code execution, but for a TLS-terminating edge proxy it means a full outage, and it needs no HTTP/2.

CVE-2026-109163: the mod_vhost_alias off-by-one that outlived its fix

This CVE shows why bounds-checking patches deserve their own review. mod_vhost_alias provides mass virtual hosting. Directives like VirtualDocumentRoot "/var/www/vhosts/%-2.0.%-1.0/htdocs" map the Host header onto a filesystem path at request time. The expansion happens in vhost_alias_interpolate(), which builds the path in a stack buffer declared char buf[HUGE_STRING_LEN], where HUGE_STRING_LEN is 8192. Before each segment is appended, a helper called vhost_alias_checkspace() decides whether the buffer needs to be flushed into r->filename and reset.

The predecessor, CVE-2026-63292, was a stack overflow reachable when VirtualDocumentRoot used a hostname specifier and LimitRequestFieldSize was raised above its 8190-byte default, which let a client send a Host header longer than 8192 bytes. Apache fixed it in 2.4.69 on October 1 (SVN r1938676, credited to Hyojae Lee and Zhen Kong) by adding a guard that rejects any single segment larger than the buffer. The flush condition itself kept a strict comparison:

c

if (*pdest + size > buf + HUGE_STRING_LEN) { /* flush */ }

When an expansion fits the buffer exactly, *pdest + size equals buf + HUGE_STRING_LEN. The strict > is false, no flush happens, and the copy leaves dest pointing one byte past the end of the buffer. The terminating NUL written afterward, *dest = '\0', lands at buf[8192], one byte beyond the array. The 2.4.71 fix changes > to >=, so an exact fit triggers the flush before the copy and the terminator stays in bounds.

A one-byte NUL overwrite is rarely a code-execution primitive on a modern build. Distribution httpd binaries ship with stack protectors, PIE, and ASLR. Clobbering the byte next to the buffer tends to corrupt a local variable or the first byte of the stack canary, and __stack_chk_fail() aborts the process.

That matches Red Hat’s score of Moderate, CVSS 5.9, with high attack complexity and availability impact only. Its statement says exploitation “terminates worker processes, resulting in a denial of service without leading to remote code execution.” Red Hat also notes that its RHEL httpd packages don’t contain the vulnerable code, and that standard configurations with explicit <VirtualHost> blocks never load the module. If you don’t use VirtualDocumentRoot or VirtualScriptAlias, this CVE doesn’t apply to you.

Five independent researchers reported the residual within days of 2.4.69 shipping, which shows how quickly incomplete fixes get diffed and retested.

How to patch Apache and confirm the fix

The remediation is to run 2.4.71 or later, from the official download or your distribution’s updated package. The traps are in how you confirm it.

  • On RHEL 9 and 10, mod_http2 ships as a separate package from httpd. The fixes landed in mod_http2 errata (RHSA-2026:98981 for RHEL 9, RHSA-2026:98978 for RHEL 10) on November 11. Updating the httpd package alone does not close CVE-2026-119618 there.
  • Standalone mod_h2 users have a harder problem. The icing/mod_h2 repository was archived on June 9, 2026, and its final release, v2.0.42 on May 29, predates the fix, so no patched standalone tag exists.
  • The practical path is to cherry-pick commit 824beab9 into your build or move to the in-tree mod_http2 that ships with httpd 2.4.71.
  • Secondary trackers have muddled the version details. Wiz’s table lists a patched “2.0.24 and newer,” and Upwind cites a nonexistent 2.0.43. Apache’s own affected range, ending at 2.0.42, is the authoritative one.

Before you declare a host clean, check what it loads and negotiates instead of trusting a banner:

bash

httpd -v
apachectl -M | grep -E 'http2_module|md_module|vhost_alias_module'
grep -rnE '^\s*(Protocols|MDomain|VirtualDocumentRoot|VirtualScriptAlias)' \
/etc/httpd/ /etc/apache2/ 2>/dev/null

mod_http2 only processes traffic if h2 or h2c appears in a Protocols directive. A server with the module loaded but HTTP/1.1-only negotiation is not exposed to the critical flaw. Upstream Apache does not enable HTTP/2 by default, so exposure depends on your configuration, which is why the audit above is worth the two minutes it takes.

Workarounds if you can’t patch right away

When you can’t patch inside a change window, the compensating controls are configuration changes:

  • Set Protocols http/1.1 to remove the HTTP/2 attack path entirely. Browsers and proxies fall back transparently via ALPN, at the cost of multiplexing and HPACK compression. Wiz recommends this as a temporary measure.
  • Unload mod_vhost_alias if you use explicit virtual hosts, per Red Hat’s mitigation.
  • Unload mod_md only if something else manages your certificates. If renewals depend on mod_md, disabling it swaps a possible crash for a guaranteed expiry outage, so those hosts should go to the front of the patch queue.

Validate changes with apachectl configtest, and prefer a graceful reload to a hard restart, which drops active connections.

What the discovery of these bugs tells us

The critical race was found by an AI-driven scanner and, independently, by a human research team at Google Cloud. Both landed on the same function within a week. This isn’t an isolated event in the httpd changelog. The 2.4.68 release credits a mod_http2 denial-of-service fix (CVE-2026-49975) to “Quang Luong of Calif.IO in collaboration with OpenAI Codex.”

Automated analysis is now finding concurrency and memory-safety bugs in mature C code fast enough to compress the defender’s timeline, and the deterministic fingerprint above means reconnaissance can be automated just as easily. The five-finder pileup on the mod_vhost_alias residual points the same way: a fix that lands on a public branch becomes a research target within days.

The lesson in the code is narrower. The critical flaw wasn’t a missing bounds check or an injection. It was an ownership violation, a write to a shared structure after it had been cloned for private use, and the compiler couldn’t see it because const on a struct doesn’t freeze the objects its pointers reference. Code review that tracks which thread owns which pool, and which tables cross the session/worker boundary, catches this class of defect. Configuration hardening reduces exposure but cannot restore the invariant. Only the patched code does.

Which team owns which CVE

One version bump here carries three different threat profiles that belong to three different owners:

  • The mod_http2 RCE is an emergency for infrastructure and platform engineering, with the SOC watching for anomalous child-process crashes and spawns.
  • The mod_md crash belongs to whoever owns TLS automation and edge ingress, a population that often runs seven-year-old httpd builds.
  • The mod_vhost_alias residual belongs to web operations and AppSec teams running multi-tenant hosting.

If all three land in one undifferentiated backlog, analysts spend the first hours working out who owns which affected host, while the fingerprintable, reliably exploitable critical sits unassigned.

what to do about Apache 2.4.71

Upgrade to 2.4.71 or later. Verify the loaded modules and negotiated protocols instead of the version banner, and use the CONTENT_LENGTH fingerprint to confirm the critical fix took effect. Treat the mod_md and mod_vhost_alias flaws according to whether those modules are live in your configuration.

The critical race is unauthenticated and remotely reachable, and according to Red Hat it is reliably exploitable against a default HTTP/2-enabled server. The patch is one line, and the exposure it closes covers the whole httpd child process and everything that process can reach.

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Your contribution powers free tutorials, hands-on labs, and security resources.

Why your support matters:
  • Writeup Access: Get complete writeup access within 12 hours
  • Zero paywalls: Keep the main content 100% free for learners worldwide

Perks for one-time supporters:
☕️ $5: Shoutout in Buy Me a Coffee
🛡️ $8: Fast-track Access to Live Webinars
💻 $10: Vote on future tutorial topics + exclusive AMA access

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

Buy Me a Coffee Button

If you like this post, then please share it:

News

Discover more from The CyberSec Guru

Subscribe to get the latest posts sent to your email!

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.