HTTP/2 HPACK Bomb

June 5, 2026 · View on GitHub

CVE-2025-XXXX · Nginx HTTP/2 HPACK Memory Amplification Attack

A proof-of-concept exploit for a remotely exploitable Denial of Service vulnerability in Nginx HTTP/2 HPACK implementation. Three compounding weaknesses allow an unauthenticated attacker to exhaust an Nginx worker's memory against any HTTP/2 endpoint — including one that returns only 404 errors.

⚠️ FOR AUTHORIZED SECURITY TESTING ONLY


Vulnerability Overview

ItemDetail
AffectedNginx ≤ 1.29.7 with HTTP/2 enabled
FixedNginx 1.29.8 (max_headers directive)
ImpactDenial of Service (memory exhaustion)
CVSSHigh
AuthenticationNone required
Downloadable contentNot required — any endpoint works

Root Cause

Three weaknesses compound into a devastating attack:

  1. HPACK Indexed Reference Bomb (~70:1 amplification) — A single dynamic table entry referenced 32,000 times per request costs 1 byte per reference on the wire but allocates ~59 bytes of server pool memory. Each stream consumes ~2.2 MB from ~33 KB of wire data.

  2. HTTP/2 Window Stall — By sending SETTINGS with INITIAL_WINDOW_SIZE=0, the attacker prevents the server from sending response DATA frames. Request pools are held while the response body remains unsent. Periodic 1-byte WINDOW_UPDATE drips reset the send_timeout timer, holding memory indefinitely.

  3. Flood Detection Blindness — The HPACK bomb traffic consists entirely of valid HPACK-encoded request headers. The overhead ratio is 1.001:1, far below the 8:1 flood detection threshold. The attack is architecturally invisible to Nginx's built-in flood check.

Impact

ConnectionsUpload (burst)Server MemorySustained BW
14 MB285 MB34 B/s
520 MB1.4 GB170 B/s
1560 MB4.2 GB510 B/s
50196 MB14.0 GB1.7 KB/s

50 connections sending 196 MB of HPACK bomb headers consumed ~14 GB of server memory in under 7 seconds.


Features

  • Zero dependencies — Pure Python stdlib (ssl, socket, struct)
  • Full HPACK bomb — 32,000 indexed references per stream (1 byte → 59 bytes)
  • Window stallINITIAL_WINDOW_SIZE=0 blocks response DATA frames
  • Memory hold — Periodic WINDOW_UPDATE drips keep memory parked indefinitely
  • Parallel connections — Multi-threaded TLS+H2 connection establishment
  • Configurable — Streams, headers, hold time, drip interval all tunable

Quick Start

Prerequisites

  • Python 3.8+
  • Target with Nginx ≤ 1.29.7 and HTTP/2 enabled

Run

# Single connection demo (~280 MB server memory)
python3 hpack_bomb.py --host target.com --port 443 -n 1

# OOM a 4 GB worker
python3 hpack_bomb.py --host target.com --port 443 -n 15

# Extended hold (1 hour)
python3 hpack_bomb.py --host target.com --port 443 -n 15 --hold 3600

# With HTTP proxy
python3 hpack_bomb.py --host target.com --port 443 -n 15 --proxy http://127.0.0.1:2080

Docker Test Environment

Spin up a vulnerable Nginx container for testing:

# Build image (uses nginx:1.29.7)
./run.sh build

# Start container (8 GB memory limit, port 8443)
./run.sh start

# Run attack
./run.sh attack15

# Monitor memory usage
./run.sh monitor

# Stop container
./run.sh stop

Environment Variables

VariableDefaultDescription
PORT8443Host port mapping
MEMORY8gContainer memory limit

Usage

usage: hpack_bomb.py [-h] [--host HOST] [--port PORT] [--proxy PROXY]
                     [-n CONNECTIONS] [--streams STREAMS] [--headers HEADERS]
                     [--hold HOLD] [--drip-interval DRIP_INTERVAL] [-v]

HPACK Bomb + HTTP/2 Window Stall — Memory Exhaustion PoC

options:
  --host HOST           Target host (default: 127.0.0.1)
  --port PORT           Target port (default: 443)
  --proxy PROXY         HTTP proxy (e.g. http://127.0.0.1:2080)
  -n, --connections N   Number of concurrent connections (default: 1)
  --streams N           Streams per connection (default: 128)
  --headers N           Headers per stream (default: 32000)
  --hold N              Hold time in seconds (default: 120)
  --drip-interval N     Seconds between WINDOW_UPDATE drips (default: 50)
  -v, --verbose         Verbose per-connection output

Parameters

ParameterDefaultDescription
--host127.0.0.1Target hostname or IP
--port443Target port
--proxyNoneHTTP proxy URL (e.g. http://127.0.0.1:2080)
-n1Number of concurrent TLS connections
--streams128HTTP/2 streams per connection (max 128)
--headers32000HPACK indexed references per stream
--hold120Seconds to hold memory after attack
--drip-interval50Seconds between WINDOW_UPDATE drips
-vfalseEnable verbose output

Memory Estimation

ConnectionsStreamsHeadersEst. MemoryUploadRatio
112832000~284 MB~4 MB70:1
512832000~1.4 GB~20 MB70:1
1512832000~4.2 GB~60 MB70:1
5012832000~14 GB~196 MB70:1

How It Works

Attack Flow

┌─────────────┐                                    ┌─────────────┐
│   Attacker  │                                    │    Nginx    │
└──────┬──────┘                                    └──────┬──────┘
       │                                                  │
       │  1. TLS + ALPN h2                                │
       │─────────────────────────────────────────────────>│
       │                                                  │
       │  2. SETTINGS: INITIAL_WINDOW_SIZE=0              │
       │─────────────────────────────────────────────────>│
       │                                                  │
       │  3. HEADERS (×128 streams × 32000 refs each)     │
       │─────────────────────────────────────────────────>│
       │                                                  │  Allocates ~2.2 MB
       │                                                  │  per stream
       │                                                  │
       │  4. Server sends HEADERS (not flow-controlled)   │
       │<─────────────────────────────────────────────────│
       │                                                  │
       │     DATA blocked (send_window = 0)               │
       │                                                  │  Memory held!
       │                                                  │
       │  5. WINDOW_UPDATE +1 (every 50s)                 │
       │─────────────────────────────────────────────────>│
       │                                                  │  Resets timeout
       │     Server sends 1 byte of DATA                  │
       │<─────────────────────────────────────────────────│
       │                                                  │
       │  ... repeat indefinitely ...                     │

HPACK Bomb Mechanism

Each HTTP/2 request contains:

  1. 4 pseudo-headers via static table indexed references (4 bytes)
  2. 1 literal-with-indexing insert: name "a", value "" (4 bytes)
  3. ~32,000 indexed references to dynamic entry 62 (`0xBE$ \times 32{,}000)

\text{Each} \text{indexed} \text{reference} \text{costs}:

  • 1 \text{byte} \text{on} \text{the} \text{wire}
  • ~59 \text{bytes} \text{of} \text{server} \text{memory} (3 \text{bytes} $state.pool+ 56 bytesngx_table_elt_t`)

This yields a ~70:1 memory amplification ratio.

Window Stall Mechanism

  1. Client sends SETTINGS_INITIAL_WINDOW_SIZE=0
  2. Server generates response → HEADERS sent immediately (not flow-controlled)
  3. DATA frame blocked → send_window = 0
  4. Body buffered → send_timeout timer starts (default 60s)
  5. Client sends 1-byte WINDOW_UPDATE every ~50s
  6. Server sends 1 byte of response → timer resets
  7. Repeat → memory held indefinitely

Detection

Suricata / Snort Rules

# Detect HPACK Bomb - High HEADERS count
alert http2 any any -> any any (
    msg:"HTTP/2 HPACK Bomb Detected";
    http2.type == 1;
    threshold:type both, track by_src, count 100, seconds 10;
    sid:1000001; rev:1;
)

# Detect INITIAL_WINDOW_SIZE=0
alert http2 any any -> any any (
    msg:"HTTP/2 Window Stall - INITIAL_WINDOW_SIZE=0";
    http2.type == 4;
    http2.settings.initial_window_size == 0;
    sid:1000002; rev:1;
)

Traffic Analysis

# Capture traffic
tcpdump -i eth0 -w capture.pcap "tcp port 443"

# Analyze with tshark
tshark -r capture.pcap -Y "http2.type == 1"     # HEADERS frames
tshark -r capture.pcap -Y "http2.type == 4"     # SETTINGS frames
tshark -r capture.pcap -Y "http2.type == 8"     # WINDOW_UPDATE frames

Mitigation

Upgrade

# Upgrade to nginx 1.29.8 or later
# which adds the max_headers directive

Nginx Configuration

# Limit headers per request (nginx 1.29.8+)
http2_max_headers 100;

# Or disable HTTP/2 entirely if not needed
# listen 443 ssl;  # Remove http2 flag
  1. Count-based header limit — Add explicit limit on header count per request
  2. Decoded-to-wire ratio limit — Reject requests where HPACK expansion exceeds threshold
  3. Minimum INITIAL_WINDOW_SIZE — Enforce minimum window size (e.g., 1024 bytes)

File Structure

.
├── hpack_bomb.py          # Main attack PoC
├── run.sh                 # Docker test environment manager
├── Dockerfile             # Vulnerable nginx:1.29.7 image
├── nginx.conf             # Default nginx config (no hardening)
├── monitor_rss.py         # RSS memory monitor (Python)
├── monitor_rss.sh         # RSS memory monitor (Bash)
├── analyze_pcap.sh        # PCAP analysis script
├── suricata_rules.rules   # IDS detection rules
└── README.md              # This file

Technical Details

Affected Code Path

// ngx_http_v2_table.c — allocates fresh copy per reference
p = ngx_pnalloc(h2c->state.pool, entry->name.len + 1);
h2c->state.header.name.data = p;

p = ngx_pnalloc(h2c->state.pool, entry->value.len + 1);
h2c->state.header.value.data = p;

// ngx_http_v2.c — no count limit, only byte limit
h = ngx_list_push(&r->headers_in.headers);  // 56 bytes per header

Flood Detection Bypass

// Nginx flood check
if (h2c->total_bytes / 8 > h2c->payload_bytes + 1048576) {
    // "http2 flood detected"
}

// Attack: total_bytes ≈ payload_bytes (ratio 1.001:1)
// Check: 4,200,000 / 8 > 4,194,304 + 1,048,576?
//        525,000       > 5,242,880?  → NO → passes

glibc Allocator Retention

When nginx destroys request pools via free(), glibc's ptmalloc retains the memory in arena free lists rather than returning it to the OS. This means:

  • A single burst permanently degrades the worker even without maintaining connections
  • Only worker restart reclaims the memory
  • nginx -s reload does NOT help (same worker PIDs)

References


Disclaimer

This tool is provided for authorized security testing and educational purposes only. Unauthorized use of this tool against systems you do not own or have explicit permission to test is illegal and unethical. The authors are not responsible for any misuse or damage caused by this tool.

Always obtain proper authorization before conducting security testing.


License

This project is provided as-is for security research purposes.