← Back to Research

The Apache HTTP/2 Double-Free Everyone Patched as a DoS (CVE-2026-23918)

"It's just a double free, the worst case is a crash." Three customers told us this in the 36 hours after Apache 2.4.67 shipped. The three customers were running Apache 2.4.66 in production and had open change-tickets that read "low-priority memory hardening, batch with the next quarterly window." CVE-2026-23918 has a CVSS of 8.8 because the same double-free is reachable on early-reset HTTP/2 frames before authentication, and the freed allocation is one any client-controlled body can re-occupy. Here is the breakdown, the Proof Capsule outline, and why the FP-rejection trap on this class of bug is the deadliest one we see.

On 4 May 2026 the Apache HTTP Server project shipped httpd 2.4.67, closing five HTTP/2-stack issues at once. The headline finding was CVE-2026-23918: a double-free in the early-reset path of mod_http2 that Bartlomiej Dmitruk of striga.ai and Stanislaw Strzalkowski of isec.pl reported on 10 December 2025. CVSS 8.8, RCE-class. Apache's own advisory uses the precise wording "double free and possible RCE on early reset" — that "possible" is the part the FP-rejection trap latches onto. Most internal triage queues will route a finding labelled "possible RCE, demonstrated DoS" to the same backlog as last week's HSTS audit. They should not.

What happened

The bug lives in mod_http2's handling of an HTTP/2 RST_STREAM frame received before the full request has been parsed. The "early reset" path is meant to clean up server-side state attached to the half-formed request. In the affected versions — 2.4.66 only, per the oss-sec advisory — the cleanup path frees a stream-bound allocation while a sibling code path still holds a reference and frees the same allocation again on its own teardown. The two frees can be made to fall on the same allocator slab on the same worker thread, with attacker-controllable content occupying the slab between the two free calls. That is the textbook double-free into use-after-free into write-what-where primitive.

The TheHackerWire breakdown walks the call graph and explains why the early-reset path is reachable pre-authentication: an attacker only needs to open an HTTP/2 connection, send the SETTINGS frame, send a HEADERS frame opening a stream, and then send RST_STREAM before the rest of the request body lands. The whole sequence fits in three packets and requires no credentials of any kind. Apache 2.4.66 is the only affected branch, but that branch was the long-term-support recommendation for any operator who tracked the “stable, not bleeding-edge” advice in the Apache docs through Q1 2026. We have seen 2.4.66 on production reverse proxies in front of healthcare scheduling apps, payment gateways, and several CMS deployments where the operator believed they had three months of cushion before they had to plan an upgrade.

The patch

The fix is 2.4.67, shipped 4 May 2026. The patch was committed in revision r1930444 on 11 December 2025, the day after the report — the five-month gap between the commit and the public release is the embargo window. The interim mitigation, if you cannot deploy 2.4.67 today, is to disable HTTP/2: remove Protocols h2 h2c from your configuration, or comment out LoadModule http2_module. That change kicks every HTTP/2 client back to HTTP/1.1, which costs you head-of-line-blocking-related performance but closes the vulnerability completely.

Why this gets mis-triaged as DoS

Three reasons, in the order we have seen them this week.

The advisory text uses "possible RCE." The word "possible" is doing a lot of work in that sentence. It is honest — the Apache security team does not have a working public RCE demonstration to point at as of this writing — but it lets a reviewer reading the advisory in a hurry classify the finding as "we should patch but it isn't urgent." A double-free with attacker-controlled allocation reuse is RCE in the same sense that Heartbleed was information disclosure: the primitive is sufficient and the only thing missing is the exploit author's time.

The reproducer in the wild today is a crash. Public PoC traffic against an unpatched 2.4.66 produces a segmentation fault and a worker-process restart. Tools that fingerprint vulnerability state by behaviour will see "child died, parent restarted" and report DoS. The behavioural signature of weaponised RCE looks identical for the first few microseconds — the difference is whether the attacker's heap-grooming traffic landed on the freed allocation before the second free. That part is invisible to a passive scanner.

Your change-management board recognises "DoS on a memory bug" as a known low-urgency category. Worker restarts are routine operational events on busy proxies. A finding that translates to "an extra worker restart per million requests on a niche HTTP/2 feature" is, on its face, the kind of thing that batches into the next quarterly upgrade. The reviewer is correct about the steady-state operational cost. They are wrong about the cost of the finding being exploitable as RCE in the four-week interval between disclosure and the day they planned to patch.

Here's what a Proof Capsule for this looks like

Memory-corruption bugs are the hardest class for the Proof Capsule format because the customer must accept that we will not provide a weaponised RCE replay against their production service. What the Capsule provides is the determinant probe: a probe that triggers the exact early-reset code path, observes the cleanup behaviour, and reports VULNERABLE, PATCHED, or HTTP2_DISABLED based on the response signature. The customer's engineering team uses the Capsule to confirm exposure, not to demonstrate an end-to-end exploit.

# capsule.yaml — CVE-2026-23918 schematic
finding_id: CELVEX-2026-23918-APACHE-HTTP2-DOUBLEFREE
vulnerability:
  cve: CVE-2026-23918
  cvss: 8.8
  class: double-free-into-use-after-free
  affected: apache httpd 2.4.66 with mod_http2 enabled
target:
  asset: edge.example-customer.com
  banner_detected: "Apache/2.4.66 (Unix)"
  http2_advertised: true
preconditions:
  - tcp/443 reachable from the test runner
  - ALPN negotiation succeeds with h2 protocol
  - mod_http2 enabled and not blocked by an upstream WAF
artifacts:
  - poc/replay.sh                   # benign early-reset prober
  - poc/expected-output.txt         # captured worker-restart log fragment
  - poc/cleanup.sh                  # nothing to clean — probe is read-only
remediation:
  patch: apache httpd 2.4.67 or later
  vendor_advisory: https://httpd.apache.org/security/vulnerabilities_24.html
  interim_mitigation: |
    # disable HTTP/2 until the upgrade window
    sudo sed -i 's/^Protocols h2 h2c/Protocols http\/1.1/' /etc/apache2/apache2.conf
    sudo systemctl reload apache2
disclosure:
  reported: 2025-12-10
  patch_committed: 2025-12-11
  public_release: 2026-05-04
  cve_assigned: 2026-05-04

The replay.sh outline for this Capsule does not produce code execution. It produces an unambiguous signal that the early-reset path is reachable on the customer's edge, then exits. Any customer engineer reading the script can verify it does no more than the comments claim.

#!/usr/bin/env bash
# replay.sh — CVE-2026-23918 prober (read-only, no exploitation)
# Sends one HTTP/2 SETTINGS, one HEADERS, one RST_STREAM, observes response.
# Outputs OK / VULNERABLE_VERSION / HTTP2_DISABLED / PATCHED.
set -euo pipefail

HOST="${1:?usage: replay.sh <host> [<port=443>]}"
PORT="${2:-443}"

# 1. version banner check
BANNER="$(curl -sI "https://$HOST:$PORT/" --http1.1 | grep -i '^server:' || true)"
case "$BANNER" in
  *"Apache/2.4.66"*) echo "VERSION_VULNERABLE: $BANNER" ;;
  *"Apache/2.4.67"*|*"Apache/2.4.68"*|*"Apache/2.4.69"*) echo "PATCHED: $BANNER" ; exit 0 ;;
  *"Apache/2.4."*) echo "OK: $BANNER (not the affected branch)" ; exit 0 ;;
  *) echo "INDETERMINATE: $BANNER" ;;
esac

# 2. ALPN h2 negotiation
if ! openssl s_client -alpn h2 -connect "$HOST:$PORT" -servername "$HOST" \
       </dev/null 2>/dev/null | grep -q "ALPN protocol: h2"; then
  echo "HTTP2_DISABLED: server did not negotiate h2 — not exposed via this vector"
  exit 0
fi

# 3. send the early-reset frame sequence using a Go HTTP/2 prober
#    (binary lives alongside replay.sh; source open in the capsule repo)
./bin/h2-early-reset-probe "$HOST" "$PORT" \
  --output /tmp/probe.json \
  --read-only-mode

# 4. classify
case "$(jq -r .verdict /tmp/probe.json)" in
  early_reset_accepted_no_crash)  echo "PATCHED: early-reset path returned cleanly" ;;
  early_reset_accepted_with_close) echo "VULNERABLE: server closed connection on early-reset (CVE-2026-23918 signature)" ; exit 2 ;;
  *)                              echo "INDETERMINATE: see /tmp/probe.json" ;;
esac

The h2-early-reset-probe binary is the load-bearing piece. It opens a TLS connection, completes the HTTP/2 preface and SETTINGS exchange, opens a stream with HEADERS, sends RST_STREAM before the body lands, and watches for the connection-close signature. It does not attempt to occupy the freed allocation. That is the line the Capsule will not cross on a customer's production edge without a separate, scoped, written engagement.

What Celvex would have caught and how customers would have verified

Honest scope: the Apache HTTP Server source tree is not in our patch-mining corpus today. We did not independently find this bug; the Apache security team and the original reporters did. What our pipeline ships within hours of public disclosure is a tagged test in the scanner corpus — WEB-APACHE-HTTP2-EARLY-RESET-23918 — that performs the version-banner check and the read-only early-reset probe described above on every customer asset that advertises an HTTP/2 endpoint. The check ran for the first time during the 4 May 2026 nightly chain. By the morning of 5 May, every customer with an exposed Apache 2.4.66 had a Proof Capsule waiting in their dashboard.

The customer-side verification is the part that closes the FP-rejection gap. The on-call engineer pulls the replay.sh from the dashboard, points it at the staging mirror of the production edge, and gets a definitive VULNERABLE / PATCHED / HTTP2_DISABLED verdict in under thirty seconds. The change-management ticket no longer reads "low-priority memory hardening, batch with quarterly window." It reads "CVE-2026-23918 reachable on edge.example.com:443; patch deployed in staging; production rollout scheduled for tonight at 02:00 UTC." The reproduction is the conversation.

This is L1.5 today. We do not weaponise the early-reset primitive against a customer's production allocator on our own initiative; that is L3 work and we have not earned the right to claim it. What we ship is the Capsule, the prober, and the scheduling discipline of running the check the night the patch lands. The platform documentation is precise about the boundary.

Mitigation guidance

  1. Deploy Apache 2.4.67 or later this week to every Internet-facing httpd. The vendor advisory covers all five HTTP/2 issues fixed in this release. The blast-radius math gets worse, not better, while you wait.
  2. Until you patch, disable HTTP/2. Remove h2 h2c from Protocols, or unload http2_module. The performance hit on HTTP/1.1 is real but bounded; the performance hit of an in-the-wild RCE is unbounded.
  3. Inventory every Apache instance, not just the obvious ones. The two assets that get patched late are the appliance-bundled httpd (printers, NAS devices, network gear management consoles) and the legacy reverse proxy nobody owns. The version banner is the only fingerprint you need; nmap -sV --script http-server-header across your perimeter inventory finds the second category in under an hour.
  4. If your edge is fronted by a CDN or WAF, do not rely on it as primary mitigation. Cloudflare, Fastly, Akamai, and the rest can rate-limit RST_STREAM frames per connection, but the ALPN negotiation that gets the attacker into HTTP/2 with your origin happens before any application-layer rule fires. The CDN buys you time. It does not replace the upgrade.
  5. Audit worker-process restart rates over the past 21 days for any spike correlating with HTTP/2 traffic from unfamiliar source ASNs. A weaponised exploit attempt that fails to land on a controlled allocation looks like a worker crash. A weaponised attempt that lands looks like nothing in your logs, which is why the audit window starts before the public disclosure date.

Bottom line

"Possible RCE" in a vendor advisory is the FP-rejection trap that disguises a critical finding as a hardening item. CVE-2026-23918 is the textbook case: a double-free reachable pre-authentication on the early-reset path, on an LTS-blessed Apache release that thousands of operators are still running because the upgrade was scheduled for next quarter. The right response is the upgrade today, the HTTP/2 disable as the bridge, and a Proof Capsule run against every Apache edge to confirm the advisory's word "possible" does not become "exploited" on your perimeter. The reproduction is the conversation. The conversation does not happen by itself.

Pen-testers hand you a PDF once a year; CELVEX Group runs every attack they would, every week, and proves the ones that still work — with a fix attached.

Sources

Run a free Exposure Check — 60 seconds, no signup

See the publicly visible signals an attacker would use to fingerprint your edge servers, your TLS configuration, and the rest of your perimeter. No account required.

Start your Exposure Check