CVE-2023-44487: HTTP/2 Rapid Reset — The DDoS That Broke Records and Rewrote Assumptions

#cve #ddos #http2 #cloudflare #appsec #protocol #exploit-analysis

In August 2023, something unusual started appearing in Cloudflare’s traffic dashboards. Attack volumes that dwarfed anything seen before — not through a massive botnet of millions of compromised machines, but through a previously unknown abuse of a protocol design property that nearly every widely deployed HTTP/2 server handles inefficiently. What followed was a coordinated, industry-wide disclosure that patched hundreds of products simultaneously and permanently changed how we think about Layer 7 DDoS. This is the full technical breakdown.


CVE Details

FieldValue
CVE IDCVE-2023-44487
CVSS v3.1 Score7.5 HIGH
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWECWE-400: Uncontrolled Resource Consumption
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredNone
User InteractionNone
ScopeUnchanged
Confidentiality / IntegrityNone
Availability ImpactHigh
First Exploited in WildAugust 2023
Public DisclosureOctober 10, 2023
CISA KEV AddedOctober 10, 2023
CISA Remediation DeadlineOctober 31, 2023

The CVSS 7.5 score understates the real-world impact. A score like this typically implies a moderately exploitable remote vulnerability — but this one generated the largest DDoS attacks ever recorded against production infrastructure at the time of disclosure.


Background: HTTP/2 Protocol Mechanics

To understand the vulnerability, you need a solid understanding of how HTTP/2 works at the wire level.

HTTP/1.1 vs HTTP/2

HTTP/1.1 is fundamentally sequential. Each request occupies a connection, and responses must be returned in order. To parallelize, browsers open multiple TCP connections (typically 6 per origin). This burns file descriptors, TCP state, and TLS session overhead on both sides.

HTTP/2 (originally RFC 7540, now superseded by RFC 9113) replaces this with stream multiplexing — multiple logical request/response pairs flow over a single TCP connection simultaneously, each identified by an integer stream ID.

HTTP/1.1 (6 separate TCP connections):
TCP 1: GET /a → response /a
TCP 2: GET /b → response /b
TCP 3: GET /c → response /c
...

HTTP/2 (1 TCP connection, many streams):
Stream 1: HEADERS(GET /a) → DATA(/a response)
Stream 3: HEADERS(GET /b) → DATA(/b response)
Stream 5: HEADERS(GET /c) → DATA(/c response)
...all interleaved on the same TCP connection

Stream IDs are odd-numbered for client-initiated streams, even-numbered for server-initiated (push). They increment monotonically and cannot be reused within a connection.

HTTP/2 Frame Structure

Every HTTP/2 message is broken into frames. The generic frame format is:

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+

Key frame types relevant to this vulnerability:

TypeCodeDescription
HEADERS0x1Initiates a new stream; carries HTTP headers
DATA0x0HTTP response/request body
RST_STREAM0x3Immediately terminates a stream
SETTINGS0x4Connection parameters negotiation
WINDOW_UPDATE0x8Flow control
GOAWAY0x7Graceful connection shutdown

Stream Lifecycle

A stream progresses through defined states per RFC 9113 §5.1 (formerly RFC 7540):

                        +--------+
                send PP |        | recv PP
               ,--------|  idle  |--------.
              /         |        |         \
             v          +--------+          v
      +----------+          |           +----------+
      |          |          | send H /  |          |
,-----|:reserved |          | recv H    |:reserved |-----.
|     | (local)  |          |           | (remote) |     |
|     +----------+          v           +----------+     |
|         |             +--------+             |         |
|         |     recv ES |        | send ES     |         |
|  send H |     ,-------|  open  |-------.     | recv H  |
|         |    /        |        |        \    |         |
|         v   v         +--------+         v   v         |
|     +----------+          |           +----------+     |
|     |   half   |          |           |   half   |     |
|     |  closed  |          | send R /  |  closed  |     |
|     | (remote) |          | recv R    | (local)  |     |
|     +----------+          |           +----------+     |
|          |                |                 |          |
|          | send ES /      |       recv ES / |          |
|          | send R /       v        send R / |          |
|          | recv R     +--------+   recv R   |          |
| send R / `----------->|        |<-----------' send R / |
| recv R                | closed |            recv R     |
`---------------------->|        |<----------------------'
                        +--------+

The critical property: streams in open or half-closed state count toward SETTINGS_MAX_CONCURRENT_STREAMS. This parameter, advertised by the server in the initial SETTINGS frame, is typically set to 100.


The Vulnerability: How Rapid Reset Actually Works

The RST_STREAM Abuse Pattern

HTTP/2 includes RST_STREAM (frame type 0x3) specifically to allow graceful cancellation of in-flight requests. The canonical use case is a browser tab being closed — the client sends RST_STREAM so the server doesn’t waste bandwidth sending a response nobody will read. This is a feature, not a bug. The bug is in what happens immediately after.

From RFC 9113 §6.4 (formerly RFC 7540):

“RST_STREAM frames MUST NOT be sent for a stream in the ‘idle’ state. If a RST_STREAM frame identifying an idle stream is received, the recipient MUST treat this as a connection error of type PROTOCOL_ERROR.”

The key insight is what happens after a client sends RST_STREAM. The stream moves to the closed state and the attacker can immediately recycle that stream slot — opening a new stream without waiting for any server acknowledgment. Exactly when the server’s internal accounting updates depends on the implementation: some decrement the counter on RST_STREAM receipt, others do it asynchronously. Either way, the upstream dispatch has almost always already happened.

This is the core problem. The attacker is never constrained by request completion. Stream slots are recycled at the rate the client can send frames, not at the rate the server finishes work.

The Attack Pattern

Client                                      Server
  |                                           |
  |-- HEADERS (stream 1, GET /expensive) ---> | → dispatch to worker/upstream
  |-- RST_STREAM (stream 1) ----------------> | ← slot freed immediately
  |-- HEADERS (stream 3, GET /expensive) ---> | → dispatch to worker/upstream
  |-- RST_STREAM (stream 3) ----------------> | ← slot freed immediately
  |-- HEADERS (stream 5, GET /expensive) ---> | → dispatch to worker/upstream
  |-- RST_STREAM (stream 5) ----------------> | ← slot freed immediately
  |-- HEADERS (stream 7, GET /expensive) ---> | → dispatch to worker/upstream
  |-- RST_STREAM (stream 7) ----------------> |
  ... (continues at wire speed)
  |                                           |
  |                                           | [worker queue saturated]
  |                                           | [upstream connections exhausted]
  |                                           | [CPU pegged at 100%]
  |                                           | → HTTP 503 / timeout to real clients

The attacker is never actually consuming stream slots from the server’s perspective — they keep cycling. But the server is dispatching real work for every HEADERS frame it receives before the RST_STREAM arrives, because in asynchronous proxy architectures the cancellation signal and the upstream dispatch are decoupled.

Why Concurrency Limits Don’t Help

You might think: just lower SETTINGS_MAX_CONCURRENT_STREAMS to 1. This doesn’t work because the rate of cycling is independent of the limit. With a limit of 1, the attacker sends HEADERS + RST_STREAM as a pair. Each pair consumes zero net slots. The throughput bottleneck moves from stream count to raw frame processing speed.

Cloudflare’s analysis with a test of 1,000 streams showed the HPACK compression effect: the first HEADERS frame is 26 bytes (full header block), but subsequent frames compress to just 9 bytes each because HPACK’s static and dynamic tables kick in. An attacker can fire thousands of request initiations per second per TCP connection.

In the Cloudflare packet capture analysis, a single packet (#15 in their trace) contained 525 HEADERS+RST_STREAM pairs — all destined to hit upstream workers simultaneously.


Discovery Timeline

This wasn’t discovered by a single researcher. Three separate cloud providers independently detected the attack pattern in production and converged on the same root cause.

DateEvent
2023-08-25Cloudflare detects unusually large HTTP attack campaigns with novel pattern
2023-08-28AWS CloudFront observes attack peaking at 155M RPS
Late AugustGoogle detects attack campaign, eventually peaks at 398M RPS
September 2023All three vendors investigate; root cause identified as HTTP/2 RST_STREAM abuse
Early OctoberCoordinated vulnerability disclosure begins with IETF, NGINX, Apache, Microsoft, F5, and ~100 other vendors
2023-10-10Public disclosure, patches released simultaneously across the industry, CISA KEV added
October–NovemberOngoing exploitation continues as unpatched servers remain

The coordination involved notifying effectively every HTTP/2 implementation maintainer on earth before going public — a logistics effort that spanned open-source projects, commercial vendors, and cloud providers simultaneously.


The Numbers: Attack Scale

This wasn’t just another large DDoS. The scale broke records by a significant margin.

Cloudflare

  • Peak: 201+ million requests per second (201,000,000 RPS)
  • Botnet size: ~20,000 machines
  • Previous Cloudflare record: ~71 million RPS
  • Ratio: Nearly 3x the previous record with ~0.3% of the machines a typical volumetric botnet uses
  • Customer impact during attack: ~1% error rate, brief spikes to 12%
  • Post-mitigation impact: Effectively zero

Google

  • Peak: 398 million requests per second
  • Duration: ~2 minutes at peak
  • Comparison: 7.5x larger than Google’s previous record (46M RPS in 2022)
  • Scale reference: More HTTP requests in 2 minutes than Wikipedia received total page views in all of September 2023

AWS

  • Peak: 155+ million requests per second
  • Active period: August 28–29 and continuing through September 2023

What 201M RPS Actually Means

A typical HTTP/2 server handles between 50,000–200,000 legitimate RPS depending on hardware. An attack generating 201 million RPS means:

201,000,000 RPS attack
÷ ~100,000 RPS server capacity
= ~2,010 servers worth of legitimate traffic being sent to one target

To illustrate the efficiency gain: if we divide 201M RPS across 20,000 machines, each machine is averaging ~10,000 RPS. An HTTP/1.1 connection can do perhaps a few hundred requests per second under ideal conditions. That same throughput over HTTP/2 requires far fewer TCP connections because stream cycling is so much cheaper than connection setup. The public disclosures don’t specify exact connection counts per bot — this is an illustrative order-of-magnitude estimate, not measured telemetry.


Affected Systems

This attack exploited a protocol design property of HTTP/2 — specifically the interaction between RST_STREAM cancellation and asynchronous upstream dispatch. RFC 9113 explicitly permits RST_STREAM and says nothing about when server-side work must be abandoned. The vulnerability isn’t a memory corruption bug hidden in the spec; it’s that nearly all widely deployed server implementations dispatched work before cancellation signals could be honored, and at scale this became catastrophic. Different architectures experienced different impact levels depending on how they handled request scheduling and stream accounting.

A note on version data: Version-specific claims age poorly. Many distributions backport security fixes without bumping the major version number, so a reported version may be patched even if it appears older than the upstream fix release. The table below reflects upstream project fix versions — always cross-reference against your vendor’s own security advisory, especially for RHEL, Ubuntu LTS, Debian, and other distribution packages that carry their own backport policies.

Web Servers

SoftwareUpstream fix versionNotes
nginx1.25.3 (mainline)Many distros backported to earlier branches — check nginx -v and your vendor advisory
Apache httpd2.4.58Fix is in mod_http2; some distributions ship as a separate package update
Apache Tomcat8.5.94, 9.0.81, 10.1.14, 11.0.0-M12Separate CVEs issued per release train
Node.jsPer October 2023 security releasesDepends on embedded nghttp2 version and Node release train
H2OUpstream patch availableCheck project releases
Envoy Proxy1.27.1 / 1.26.5 / 1.25.8 / 1.24.9Check your minor version line
HAProxyPatch availableBackports exist for earlier stable branches
Caddyv2.7.5Inherits Go’s HTTP/2 fix (go1.21.3)

Language Runtimes & Frameworks

SoftwareNotes
Go net/httpgo1.21.3, go1.20.10 — also check golang.org/x/net dependency if used separately
.NET / KestrelPatched October 10, 2023 — apply via Windows Update or .NET runtime update
Python hyper-h2Patch available; impact depends on whether server does async dispatch
gRPC (all languages)Upstream patches per language runtime — check grpc.io advisories
Netty (Java)4.1.100.Final
Jetty9.4.53, 10.0.17, 11.0.17, 12.0.2
Undertow (JBoss)Patch released; included in EAP updates

Cloud Platforms

ProviderImpactStatus
CloudflareActive attack, 201M RPS peakMitigated with protocol-level fix + IP Jail extension
Google CloudActive attack, 398M RPS peakMitigated at edge
AWS CloudFrontActive attack, 155M RPS peakMitigated, customers auto-protected
Azure / IISHTTP.sys and Kestrel affectedPatched October 10, 2023

Microsoft Products

Microsoft released patches across:

  • Windows Server (HTTP.sys kernel driver)
  • IIS
  • .NET (Kestrel server)
  • Azure (SaaS/PaaS auto-patched; IaaS required manual update)

Palo Alto Networks

PAN-OS itself is not a directly vulnerable HTTP/2 server in the typical attack path. However, PAN-OS devices with HTTP/2 traffic inspection enabled during an active DDoS may be affected. Mitigation: Threat Prevention ID 40152 (Applications and Threats content update 8765).


Deep Dive: Packet-Level Analysis

Let’s look at what this attack looks like at the binary level.

Normal HTTP/2 Connection Setup

Client → Server: TCP SYN
Server → Client: TCP SYN-ACK
Client → Server: TCP ACK
Client → Server: TLS ClientHello (ALPN: h2)
Server → Client: TLS ServerHello (ALPN: h2)
... TLS handshake ...
Client → Server: HTTP/2 Connection Preface: PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
Client → Server: SETTINGS frame (initial settings)
Server → Client: SETTINGS frame (SETTINGS_MAX_CONCURRENT_STREAMS=100)
Client → Server: SETTINGS ACK
Server → Client: SETTINGS ACK
... connection established ...

Normal Request Flow

Stream 1 (client-initiated):
Client → Server: HEADERS frame (stream_id=1, flags=END_HEADERS)
  :method = GET
  :path = /
  :scheme = https
  :authority = example.com

Server → Client: HEADERS frame (stream_id=1, flags=END_HEADERS)
  :status = 200
  content-type = text/html

Server → Client: DATA frame (stream_id=1, flags=END_STREAM)
  [response body]

Rapid Reset Attack Flow

Attack stream (repeated thousands of times per second):

Client → Server: HEADERS frame (stream_id=1, flags=END_HEADERS, 26 bytes)
  :method = GET
  :path = /
  :scheme = https
  :authority = target.com

Client → Server: RST_STREAM frame (stream_id=1, error_code=NO_ERROR, 9 bytes)

Client → Server: HEADERS frame (stream_id=3, flags=END_HEADERS, 9 bytes*)
  [HPACK-compressed, nearly identical to stream 1]

Client → Server: RST_STREAM frame (stream_id=3, error_code=NO_ERROR, 9 bytes)

Client → Server: HEADERS frame (stream_id=5, ...) 9 bytes
Client → Server: RST_STREAM frame (stream_id=5, ...) 9 bytes

... stream IDs increment: 7, 9, 11, 13, ... (no upper bound respected)

*After the first request, HPACK compression reduces HEADERS to ~9 bytes because the entire :method, :scheme, :authority combination is in the dynamic table. The per-request cost drops to effectively 18 bytes of attack payload.

RST_STREAM Frame Structure

+-----------------------------------------------+
|              Length: 4 (0x000004)             |
+---------------+---------------+---------------+
|  Type: 0x03   |  Flags: 0x00  |
+-+-------------+---------------+-------------------------------+
|0|              Stream ID: 1 (0x00000001)                      |
+=+=============================================================+
|          Error Code: 0x00000000 (NO_ERROR)                  |
+---------------------------------------------------------------+

Total: 9 bytes. This tiny frame resets a stream that may be running a database query, spawning a thread, or generating a large response upstream.

Wireshark Detection Filter

If you’re capturing traffic and want to see RST_STREAM frames:

http2.type == 3 && http2.flags == 0x00

Important caveat: RST_STREAM frames are normal and expected in legitimate HTTP/2 traffic — browsers send them routinely when cancelling prefetch requests, closing tabs, or aborting navigations. The filter above is useful for isolating the frame type, but it is not a reliable attack detector on its own.

What indicates Rapid Reset is not the presence of RST_STREAMs but their density relative to completed requests. A healthy connection might have an RST rate of a few percent of streams. An attack exhibits near-100% RST rate with stream IDs incrementing monotonically at wire speed.

The frame.time_delta < 0.001 timing filter is too coarse to be meaningful — capture location, network jitter, and packet coalescing all affect frame timing. Instead, look at RST count per source IP over a fixed interval relative to responses sent.

For a high-level pcap analysis using tshark:

tshark -r capture.pcap \
  -Y "http2.type == 3" \
  -T fields \
  -e frame.number \
  -e ip.src \
  -e http2.streamid \
  | awk '{print $2}' | sort | uniq -c | sort -rn | head -20

Proof-of-Concept: Code Analysis

The PoC from ThreatLab Indonesia demonstrates the core mechanism cleanly. I’m walking through the relevant sections below for educational and defensive analysis — understanding attacker tooling is essential for building effective detections.

Dependencies:

pip install httpx==0.24.0 h2==4.1.0 tqdm==4.66.1 xlsxwriter==3.1.6

The h2 library provides low-level HTTP/2 framing. httpx handles the higher-level HTTP/2 client for the initial support check.

HTTP/2 Support Detection

def check_http2_support(url):
    try:
        with httpx.Client(http2=True, verify=False, timeout=10) as client:
            response = client.get(url)
        if response.http_version == 'HTTP/2':
            return (True, "HTTP/2 Supported")
        else:
            return (False, f"Downgraded to {response.http_version}")
    except httpx.RequestError as e:
        return (False, f"Error during request: {str(e)}")

This checks whether a server actually negotiates HTTP/2 via ALPN. A server might listen on port 443 but not support HTTP/2 — in that case, it’s not vulnerable to this specific attack vector.

Core RST_STREAM Injection

def send_rst_stream_h2(host, port, stream_id, uri_path='/'):
    ssl_context = ssl.create_default_context()
    ssl_context.check_hostname = False
    ssl_context.verify_mode = ssl.CERT_NONE

    # Raw TCP + TLS socket — bypasses httpx to get direct frame control
    sock = ssl_context.wrap_socket(
        socket.socket(socket.AF_INET, socket.SOCK_STREAM),
        server_hostname=host
    )
    sock.settimeout(10)
    sock.connect((host, port))

    # Initialize HTTP/2 state machine
    config = H2Configuration(client_side=True)
    h2_conn = H2Connection(config=config)
    h2_conn.initiate_connection()  # sends connection preface + SETTINGS
    sock.sendall(h2_conn.data_to_send())

    # Send HEADERS to open a stream
    headers = [
        (':method', 'GET'),
        (':authority', host),
        (':scheme', 'https' if port == 443 else 'http'),
        (':path', uri_path)
    ]
    h2_conn.send_headers(stream_id, headers)
    sock.sendall(h2_conn.data_to_send())

    # Immediately send RST_STREAM — this is the attack
    h2_conn.reset_stream(stream_id)
    sock.sendall(h2_conn.data_to_send())
    sock.close()

    return (True, "RST_STREAM sent successfully")

The key is the direct use of raw TLS sockets with the h2 state machine. This gives byte-level control over the HTTP/2 framing, allowing HEADERS and RST_STREAM to be sent in immediate succession before any server response arrives.

In a weaponized version, the outer loop sends these pairs continuously in a tight loop across many concurrent goroutines/threads/async tasks. The PoC above demonstrates the single-shot mechanism — one HEADERS/RST pair per connection. This is enough to verify the frame exchange is accepted, but it does not confirm whether the server is actually vulnerable to the DoS at scale.

Usage

# Check a single endpoint
python3 main.py --url https://target.example.com

# Bulk assessment from a file
python3 main.py --bulk urls.txt --output results.xlsx

# Custom port (e.g., non-standard HTTPS)
python3 main.py --url https://target.example.com --port 8443

Only run this against systems you own or have explicit written authorization to test.


Checking If Your Infrastructure Is Vulnerable

Method 1: HTTP/2 Support Check (Quick)

If your server doesn’t speak HTTP/2, you’re not exposed to this specific attack vector (though you should still enable HTTP/2 — just make sure you’re patched).

# Check via curl
curl -sI --http2 https://yourdomain.com 2>&1 | grep -i "HTTP/"

# Using openssl to check ALPN negotiation
openssl s_client -alpn h2 -connect yourdomain.com:443 </dev/null 2>&1 | grep -E "ALPN|Protocol"

# Using nmap
nmap -p 443 --script http2-request yourdomain.com

Method 2: Version Checks

# nginx
nginx -v  # Needs 1.25.3+ or backported patch

# Apache httpd
httpd -v  # Needs 2.4.58+
apachectl -M | grep http2  # Verify mod_http2 is loaded

# Node.js
node --version  # Check against security advisory matrix

# Check .NET runtime
dotnet --version

Method 3: The PoC Tool

Using the ThreatLab Indonesia PoC against your own infrastructure (in a controlled test environment or with proper authorization):

git clone https://github.com/threatlabindonesia/CVE-2023-44487-HTTP-2-Rapid-Reset-Exploit-PoC
cd CVE-2023-44487-HTTP-2-Rapid-Reset-Exploit-PoC
pip install -r requirements.txt
python3 main.py --url https://your-test-server.example.com

The tool outputs results like this:

[
  {
    "Timestamp": "2023-10-15 14:32:11",
    "URL": "https://your-test-server.example.com",
    "HTTP/2 Support": "Yes",
    "Vulnerable": "VULNERABLE",
    "Details": "RST_STREAM sent successfully"
  }
]

Critical caveat — this output is misleading. The "Vulnerable": "VULNERABLE" label means the server accepted a HEADERS frame followed by an RST_STREAM frame. Every compliant HTTP/2 server will do this — including fully patched ones — because RST_STREAM is a legal and expected part of the protocol. A patched server that properly tracks RST rates will still accept a single RST_STREAM without complaint. It will only reject or throttle them at sustained high rates.

What this PoC actually confirms:

  1. The server supports HTTP/2 — which is the only thing that genuinely matters for exposure assessment
  2. The connection path isn’t filtered — the frames reached the server

What it does not confirm:

  • Whether the server is susceptible to resource exhaustion from sustained rapid cycling
  • Whether rate limiting or protocol-level mitigations are in place
  • Whether the server is patched

A "SAFE" result typically means the server doesn’t speak HTTP/2 at all, or the connection failed — in which case this specific attack vector isn’t applicable. But HTTP/2 support alone is not evidence of exploitability.

The only reliable confirmation of mitigation is applying vendor patches and verifying the version.

Method 4: Monitor RST_STREAM Rates

If you have access to server metrics or logs, an anomalous RST_STREAM rate is a strong indicator of an active attack or probe:

# If you have access to nginx with http2 debug logging:
grep "http2" /var/log/nginx/error.log | grep -i "rst_stream" | wc -l

# netstat to monitor connection states during suspected attack
ss -s
netstat -an | awk '/ESTABLISHED/ {print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head

Mitigation Strategies

Mitigation falls into three categories: patch, configure, and absorb.

1. Patch — The Only Real Fix

Apply vendor patches. There is no configuration change that fully mitigates the underlying issue without patching; the fixes require changes to the HTTP/2 state machine implementation.

# nginx: update to 1.25.3+ (mainline) or apply backport patch
# Ubuntu/Debian
sudo apt update && sudo apt upgrade nginx

# RHEL/CentOS
sudo yum update nginx

# Verify
nginx -v && nginx -T | grep "http2"

For Apache:

# Apache 2.4.58+ includes the fix
sudo apt install apache2=2.4.58*   # adjust for your distro

# Verify mod_http2 version
apache2ctl -M | grep http2

For Go applications:

go get -u golang.org/x/net
# Update go.mod to use go1.21.3+ or go1.20.10+

For .NET:

# Apply Windows Update KB5031445 or equivalent
# Or update .NET SDK/Runtime to patched version
dotnet sdk check

2. Server-Side Rate Controls

Even on patched servers, adding RST_STREAM rate limits provides defense-in-depth:

nginx (1.25.3+ with native Rapid Reset protection):

Note: http2_max_requests was removed from nginx in version 1.19.7 (2021) and will produce a startup warning or error if present in modern configs. Do not use it. F5’s official guidance for CVE-2023-44487 mitigation is to keep keepalive_requests near its default value of 1000 — inflating it to very high values (100,000+) significantly amplifies attack impact.

http {
    # Keep near default — do NOT inflate this to 100,000+.
    # F5 guidance: high values amplify Rapid Reset impact.
    keepalive_requests 1000;

    # Limit connections per IP
    limit_conn_zone $binary_remote_addr zone=per_ip:10m;
    limit_req_zone $binary_remote_addr zone=req_zone:10m rate=100r/s;

    server {
        listen 443 ssl;
        http2 on;   # nginx 1.25.x syntax; older: listen 443 ssl http2

        limit_conn per_ip 20;
        limit_req zone=req_zone burst=200 nodelay;

        # Idle and recv timeouts — tighten to reduce hanging connections
        http2_recv_timeout 30s;
        http2_idle_timeout 3m;
    }
}

Apache httpd (mod_http2 settings post-patch):

<IfModule mod_http2.c>
    # Limit concurrent streams per connection
    H2MaxSessionStreams 100

    # Reset stream tolerance (patched versions have this)
    H2ResetGracePeriod 30
    H2ResetLimit 1000

    # Reduce window size to limit amplification
    H2WindowSize 65535

    # Timeout idle connections faster
    H2StreamMaxMemSize 65536
</IfModule>

3. Cloudflare-Specific Mitigations

If you’re behind Cloudflare (as gugualiunal.com is), you benefit from protocol-level protection at the edge. Cloudflare extended its 2019 HTTP/2 DoS protections to track RST_STREAM rates per connection and close abusive connections before they reach your origin.

The real protection here is the protocol-level RST_STREAM rate tracking Cloudflare operates at the edge — that happens transparently before traffic reaches your origin. WAF rules cannot inspect HTTP/2 framing directly because Cloudflare terminates TLS and proxies HTTP/1.1 to your origin; by the time a request reaches the WAF rule engine, the RST_STREAM has already been handled (or rejected) at the protocol layer.

What you can add in the WAF for general abuse filtering — not Rapid Reset detection specifically:

(http.request.method eq "GET" and 
 cf.threat_score gt 30 and 
 not cf.bot_management.verified_bot)

With rate limiting:

Rate: 500 requests per 10 seconds per IP
Action: Block

This is generic high-threat-score filtering. It may catch some botnet traffic involved in a Rapid Reset campaign, but it is not a protocol-level mitigation and should not be relied upon as one.

The “IP Jail” mechanism Cloudflare deployed is particularly powerful — abusive IPs are blocked from using HTTP/2 to any Cloudflare customer for a cooldown period, not just the targeted property.

4. Disable HTTP/2 (Emergency Only)

If you cannot patch immediately, disabling HTTP/2 eliminates the attack vector entirely at the cost of performance:

# nginx: remove http2 from listen directive
server {
    listen 443 ssl;  # NOT: listen 443 ssl http2;
    # ...
}
# Apache: disable mod_http2
a2dismod http2
systemctl restart apache2
# Verify HTTP/2 is disabled
curl -sI --http2 https://yourdomain.com 2>&1 | grep HTTP
# Should return HTTP/1.1

This should be temporary. HTTP/2 provides real performance benefits and HTTP/3 (QUIC) is the direction the web is heading — disabling modern protocols is a stopgap, not a strategy.

5. WAF / CDN Layer

If you’re self-hosting without a CDN:

  • AWS Shield Advanced + WAF: Automatic Layer 7 DDoS mitigation with managed rule groups
  • Azure WAF: Enable HTTP/2 rate limit rules on Front Door or Application Gateway
  • Cloudflare: Free tier WAF custom rules + rate limiting protect origins; Cloudflare itself absorbed attacks at the edge
  • Fastly, Akamai: Both deployed mitigations at their edge layers during the incident

Case Studies: How Each Vendor Responded

Cloudflare

Cloudflare was first to publicly document the technical details. Their mitigation was multi-layered:

  1. Protocol layer: Extended existing HTTP/2 DoS protections (originally built in 2019) to monitor RST_STREAM frame rates. Connections exceeding thresholds are torn down while preserving legitimate uses of request cancellation.

  2. IP Jail expansion: Cloudflare’s “IP Jail” previously isolated IPs from specific targeted properties. It was expanded so that attacking IPs lose HTTP/2 access across all Cloudflare properties. Given that most botnet IPs rotate rapidly (many new IPs appeared and disappeared within a single day), this cross-property block significantly degrades attack efficiency.

  3. Infrastructure redesign: The attack exposed an architectural fragility — the pipe between their TLS termination layer and business logic proxy could saturate. They decoupled these services and introduced connection-level logging at the service boundary, enabling faster DDoS detection at the infrastructure level instead of relying on aggregate metrics.

  4. Observability gap closed: Because their HTTP analytics logs were emitted by the business logic proxy (which was being overwhelmed), attack traffic was invisible in customer dashboards during the incident. Service-level logging was added at earlier stages of the pipeline.

Google Cloud

Google’s response highlighted the scale asymmetry. The 398 million RPS peak came from a botnet that, by traditional volumetric standards, was small. Google absorbed it through their global anycast edge network capacity — the same infrastructure that routes Google Search traffic.

Their Adaptive Protection product (AI-powered DDoS detection in Cloud Armor) proved effective at identifying the novel pattern and propagating mitigation signatures in near-real-time across edge nodes.

Microsoft

Microsoft’s response was notable for its breadth. They had to patch across the full stack:

  • HTTP.sys (the kernel-mode HTTP driver used by IIS) — required a Windows patch
  • Kestrel (.NET’s built-in HTTP server) — required a .NET security release
  • Azure — SaaS/PaaS customers were auto-patched; IaaS required customer action

The October 10 Patch Tuesday release was coordinated to include the HTTP/2 fixes across all affected Windows Server and .NET versions simultaneously.

AWS

AWS CloudFront’s globally distributed edge network absorbed the attack without requiring customer intervention. Customers running behind CloudFront with AWS Shield were protected automatically. Customers exposing HTTP/2 endpoints directly (EC2, EKS, self-managed Nginx/Apache on EC2) required manual patching.


Broader Implications

Amplification Without a Reflector

Traditional volumetric DDoS amplification abuses protocols with asymmetric response sizes — DNS (small query, large response), NTP (monlist), memcached. HTTP/2 Rapid Reset is different: the amplification is in work, not bytes. A 9-byte RST_STREAM can trigger a database query, a cache miss, a file read, a TLS handshake upstream — work that consumes far more than 9 bytes worth of server resources.

This is work amplification at the application layer, and it doesn’t require reflection or spoofing. It’s a direct-path attack that benefits from the attacker’s own TCP connection being legitimate and encrypted.

The Botnet Efficiency Problem

20,000 machines generating 201 million RPS means each machine averages ~10,000 RPS. For comparison, a single HTTP/1.1 machine in a typical botnet generates maybe 100–1,000 RPS (limited by connection setup overhead). The 10-100x efficiency multiplier from HTTP/2 multiplexing means a botnet that would have been a nuisance under HTTP/1.1 becomes catastrophic under HTTP/2.

HTTP/3 (QUIC, RFC 9114) deserves its own paragraph here. After CVE-2023-44487, researchers immediately looked at QUIC’s stream cancellation behavior. HTTP/3 has RESET_STREAM and STOP_SENDING frames that serve analogous purposes to HTTP/2’s RST_STREAM. QUIC runs over UDP rather than TCP, which means the connection-establishment overhead that limits HTTP/1.1 botnet throughput is even lower — a concern worth tracking.

QUIC’s connection IDs and stream lifecycle have materially different properties from HTTP/2 streams (different flow control, different state machine, no Head-of-Line blocking at the transport layer), so a direct port of the Rapid Reset technique doesn’t apply identically. But the fundamental architectural tension — cheap stream initiation, expensive upstream work, delayed cancellation accounting — remains true for any multiplexed protocol. Expect this class of vulnerability to recur.

The HTTP/2 spec itself was also updated: RFC 9113 (published June 2022, widely adopted post-CVE-2023-44487) includes clarified guidance on RST_STREAM handling and explicitly notes that implementations should track and limit reset rates. For new deployments, RFC 9113 is the current reference — RFC 7540 is technically obsolete.

Responsible Disclosure at Scale

The coordinated disclosure for CVE-2023-44487 is one of the largest in terms of affected vendor count. Notifying nginx, Apache, Tomcat, Node.js, Go, .NET, Netty, Jetty, Undertow, H2O, Envoy, HAProxy, Caddy, every major cloud provider, and every major CDN simultaneously — and getting patches shipped from all of them within weeks — is a logistics achievement worth noting. The alternative (public disclosure before patches) would have enabled a far more damaging exploitation window.


References