The recent http2-bomb paper shows the attack leveraged against nginx, which got me curious if OpenResty was also vulnerable to the same class of attacks, and what mitigations can be put into place. This repo is a POC and testlab for that work.
Note
This is a self-contained lab that reproduces the HTTP/2 Bomb (hpack indexed-reference bomb + a slowloris window stall) against a sidecar's config, then proves the mitigations work. Everything runs locally against a throwaway container. Never point the bomb at infrastructure you do not own and I take no liability for this software's {mis,}use.
The bomb seeds one HPACK dynamic-table entry (a: with an empty value) and then
sends thousands of one-byte indexed references to it. Each one-byte reference
forces the server to allocate a fresh header (~59 bytes of pool memory). A
SETTINGS_INITIAL_WINDOW_SIZE=0 plus periodic one-byte WINDOW_UPDATE drips
keep the response from ever finishing, pinning every allocation in memory. See
RFC 7541 §7.3, Memory Consumption for the threat the spec acknowledged and
underspecified.
The lab models many production sidecar's decisive knob, large_client_header_buffers 32 32k,
which sets the HTTP/2 per-stream header budget to 1 MB instead of the nginx
default of 32 KB. That single setting makes the per-stream blast radius about
32 times larger than the values in the research paper.
- Docker running.
- Host Python 3 (standard library only; the bomb has no dependencies).
- The lab binds host port 8443 by default. Override with
PORT.
./run.sh build # build the OpenResty test image
./run.sh start vuln # start with the prod-mirroring config (4 GB cap)
./run.sh alpn # confirm the sidecar negotiates h2
./run.sh version # confirm the nginx core version (max_headers check)
# in a second terminal, watch the worker
./run.sh monitor
# fire the bomb that fills the 1 MB budget
./run.sh attack-prodThen A/B the mitigations:
./run.sh start hardened # 4 32k buffers, 32 streams, 10s timeout, limit_conn
./run.sh attack-prod # same bomb, watch RSS stay flat
./run.sh start h2off # http2 disabled
./run.sh attack-prod # client cannot even negotiate h2./run.sh with no argument prints the full command list.
nginx.vuln.conf. Mirrors many production sidecar's HTTP/2-relevant settings,
including large_client_header_buffers 32 32k. This is the baseline.
nginx.hardened.conf. Keeps size at 32k so long request lines and presigned
URLs still fit, but cuts number to 4, so the budget drops to 128 KB. Adds
http2_max_concurrent_streams 32, send_timeout 10s, and a per-IP limit_conn.
nginx.h2off.conf. Drops the http2 directive entirely. HPACK has no surface,
so the bomb cannot run. This is the complete mitigation until OpenResty ships the
max_headers directive from nginx 1.29.8.
One connection, 16 streams, 1,000,000 references per stream, about 15 MB on the wire. The container core is OpenResty 1.29.2.5 (nginx 1.29.2), capped at 4 GB.
| Config | header_limit |
Peak worker RSS | Outcome |
|---|---|---|---|
| vuln | 1 MB | 1,287 MB | Memory pinned for the hold; about 84:1 amplification |
| hardened | 128 KB | 13 MB | Connection killed on the first oversized header block |
| http2 off | n/a | 11 MB | ALPN refuses h2; client gets NO_APPLICATION_PROTOCOL |
The vuln number is from only 16 streams. Many production defaults are 128 concurrent streams per connection, so a single connection scales to roughly 10 GB.
| File | Purpose |
|---|---|
Dockerfile |
OpenResty alpine plus a self-signed cert and monitoring tools |
nginx.vuln.conf |
Prod-mirroring config (vulnerable) |
nginx.hardened.conf |
Hardened config (mitigations applied) |
nginx.h2off.conf |
HTTP/2 disabled (complete mitigation) |
hpack_bomb.py |
The PoC client, adapted to fill the 1 MB budget |
monitor_rss.py |
In-container per-worker RSS sampler |
run.sh |
Build, start, attack, and observe wrapper |