Feb 10, 2026·6 min read·1 visit
Attackers can trick the amphp/http-server into an infinite loop of allocating and deallocating HTTP/2 streams. By forcing the server to issue an RST_STREAM, the stream slot is freed immediately, allowing the attacker to open a new one instantly. This results in a Denial of Service (DoS) via resource exhaustion.
A critical logic flaw in the amphp/http-server implementation of the HTTP/2 protocol allows attackers to trigger an unbounded stream of server-side resets. By rapidly opening streams and forcing the server to reset them ('MadeYouReset'), an attacker can exhaust server CPU and memory resources on a single TCP connection, effectively bypassing standard concurrency limits.
Async PHP. To the uninitiated, it sounds like a contradiction in terms—like 'dry water' or 'secure IoT'. But libraries like amphp have pushed the boundaries, creating a non-blocking, event-driven ecosystem that allows PHP to behave more like Node.js or Go. It’s brilliant, fast, and complex.
But complexity is the devil's playground. The vulnerability in question, dubbed MadeYouReset (CVE-2025-8671), targets amphp/http-server, the component responsible for handling incoming HTTP requests. Specifically, it attacks the HTTP/2 implementation. HTTP/2 is a binary beast that allows multiplexing—sending multiple requests (streams) over a single TCP connection.
Usually, servers protect themselves by limiting the number of concurrent streams (often 100). Once you hit that limit, you wait. But this vulnerability exploits a logical loophole: what happens when a stream is closed abnormally? The server tries to be helpful, cleans up the mess, and opens the door for the next request. That helpfulness is exactly what we're going to exploit.
The core of the issue lies in how the server handles the lifecycle of a stream. In a healthy HTTP/2 conversation, the client opens a stream, sends data, the server responds, and the stream closes gracefully. But HTTP/2 also has an RST_STREAM frame—a panic button used to abruptly terminate a stream when things go wrong.
In amphp/http-server, when the server decided to reset a stream (perhaps because the client sent malformed data or violated flow control), it effectively wiped the slate clean immediately. It cancelled the internal tasks, removed the stream from the tracking array, and—here is the fatal flaw—incremented the remainingStreams counter.
This created a 'Zero-Cost Abort' loop. An attacker doesn't need to wait for a request to finish. They just need to open a stream and immediately do something annoying enough to make the server reset it. Because the server immediately frees up the slot, the attacker can open another stream in the very next frame. There is no penalty, no cooldown, and no memory of the previous error. It’s like a bouncer kicking a drunk patron out the front door but leaving the back door unlocked and unmonitored.
Let's look at the vulnerable code in src/Driver/Http2Driver.php. Before the patch, the releaseStream method was dangerously simple. It focused entirely on resource cleanup without considering behavioral heuristics.
// PRE-PATCH VULNERABLE LOGIC
private function releaseStream(int $id): void {
if (isset($this->streams[$id])) {
unset($this->streams[$id]);
// The fatal flaw: blindly allowing a new stream immediately
$this->remainingStreams++;
}
// ... cleanup logic ...
}The fix, introduced in versions 2.1.10 and 3.4.4, is substantial. The maintainers realized that not all stream closures are equal. They introduced the concept of Exceptional Releases—streams that ended because of an error or a reset.
Here is the logic in the patch. Note the introduction of a "sliding window" to track bad behavior:
// PATCHED LOGIC
private const STREAM_BEHAVIOR_WINDOW = 10; // 10 seconds
private const RESET_STREAM_RATIO = 0.25; // 25% max error rate
private function releaseStream(int $id, bool $exceptional = false): void {
// Track this release in our sliding window
$this->releasedStreams[] = [$now, $exceptional];
// ... (pruning old entries) ...
// If we have too many streams and too many errors...
if ($count > self::STREAM_BEHAVIOR_THRESHOLD &&
$exceptionalCount / $count > self::RESET_STREAM_RATIO) {
// KILL THE CONNECTION
$this->close(Http2Parser::ENHANCE_YOUR_CALM, "Stream behavior violation");
return;
}
$this->remainingStreams++;
}The server now enforces a "three strikes" style rule. If you are rapidly cycling streams and causing resets, you don't just get your stream reset—you get the entire TCP connection nuked with the error code ENHANCE_YOUR_CALM (yes, that is a real HTTP/2 error code).
To exploit this, we don't need a botnet. We just need a single client capable of speaking raw HTTP/2 frames. The goal is to maximize the "Churn Rate"—the number of stream allocations and deallocations per second.
SETTINGS frame to initiate the connection.HEADERS frame to open Stream ID N. This allocates memory on the server (Request object, buffers, parsers).N. A WINDOW_UPDATE with a specific invalid increment or a malformed header block works perfectly. The goal is to trigger the server's validation logic to say "Invalid, RESET this stream."remainingStreams.N by 2 (client streams are odd) and repeat.Because the server is essentially doing busy-work (allocating objects only to destroy them), the CPU usage spikes to 100%. If the attacker uses multiple threads over a single connection, they can exhaust memory fragmentation or simply starve legitimate requests from being processed by the event loop.
This vulnerability is particularly nasty because it is a Layer 7 denial of service. Standard network firewalls won't see a flood of packets; they'll just see a single, persistent TCP connection sending data at a moderate rate. The amplification happens inside the application logic.
For amphp, which is often used to power long-running applications like WebSockets servers or high-performance APIs, a single malicious client can stall the entire event loop. In an async model, if the loop is busy processing garbage stream resets, it isn't accepting new connections or servicing existing ones. The application simply freezes.
Furthermore, because this exploits the internal accounting of the server, it bypasses standard configuration limits like concurrent_stream_limit. You aren't violating the concurrency limit; you are adhering to it perfectly, just at warp speed.
The fix is mandatory. If you are running amphp/http-server version < 2.1.10 or < 3.4.4, you are vulnerable. There is no configuration workaround that effectively mitigates this without patching the code, as the flaw is in the core driver logic.
v2.1.10.v3.4.4.composer.lock to ensure the package version has actually updated.GOAWAY frames with error code 0xB (ENHANCE_YOUR_CALM). This indicates the new protection mechanism is triggering, which could mean you are under attack (or you have a very broken client).If patching is impossible (why?), you would need to place a sophisticated HTTP/2-aware WAF in front of the application that can track stream reset ratios per connection, but honestly, just update the library. It's easier.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
amphp/http-server amphp | < 2.1.10 | 2.1.10 |
amphp/http-server amphp | >= 3.0.0 < 3.4.4 | 3.4.4 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2025-8671 |
| GHSA ID | GHSA-8GRV-JQ2G-CFHW |
| CVSS Score | 7.5 (High) |
| Attack Vector | Network (HTTP/2) |
| CWE | CWE-404 (Improper Resource Shutdown) |
| Impact | Denial of Service (DoS) |
Improper Resource Shutdown or Release