CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-21435
5.3

The Infinite Goodbye: Choking WebTransport with Flow Control

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 12, 2026·6 min read·7 visits

PoC Available

Executive Summary (TL;DR)

A DoS vulnerability in `webtransport-go` allows attackers to hang server sessions indefinitely by withholding QUIC flow control credits during closure. This causes the server's `CloseWithError` function to block forever, leading to goroutine leaks and resource exhaustion. Fixed in v0.10.0.

In the world of async networking, shutting down a connection is supposed to be a graceful handshake. But for `webtransport-go` prior to v0.10.0, it became a death grip. A vulnerability in the session closure logic allows a malicious peer to perform a Denial of Service (DoS) by simply refusing to acknowledge the end of the conversation. By withholding QUIC flow control credits on the critical `CONNECT` stream, an attacker can force the server to wait indefinitely for a write permission that never comes. The result? A massive leak of goroutines and memory, silently suffocating the application while it politely waits to say 'goodbye'.

The Hook: When Polite Protocols Turn Toxic

WebTransport is the cool new kid on the block, designed to replace the aging WebSockets with something built on the robust, multiplexed foundation of HTTP/3 and QUIC. It promises low latency, unreliable datagrams for gaming, and reliable streams for data. Ideally, it’s the future. But like many modern protocols, it relies heavily on the cooperative nature of its underlying transport: QUIC.

Here’s the thing about QUIC: it uses a credit-based flow control system. If I want to send you data, I have to wait until you tell me you have space for it. It's polite. It prevents fast senders from overwhelming slow receivers. But what happens when that politeness is weaponized? What if I, the malicious client, initiate a session, exchange some data, and then—right when you try to hang up—I simply stop processing? I don't close the connection. I just stop issuing 'credits'.

In webtransport-go (before v0.10.0), this scenario wasn't handled gracefully. It was a deadlock trap. The server, trying to be a good citizen, attempts to send a WT_CLOSE_SESSION capsule to formally end the session. It asks the underlying QUIC stream, 'Can I write this?' The stream says, 'Wait for credit.' And the server waits. And waits. And waits. Meanwhile, that goroutine is pinned, that memory is allocated, and the server is slowly bleeding out resources, all because it’s too polite to just slam the phone down.

The Flaw: A Fatal Dependency on Flow Control

To understand this bug, you have to look at how WebTransport layers over HTTP/3. A WebTransport session is essentially bootstrapped over an HTTP/3 CONNECT stream. This stream is the control channel. When you want to close the session, the protocol dictates sending a specific capsule: WT_CLOSE_SESSION.

The vulnerability lies in the assumption that the CONNECT stream is always writable during the shutdown phase. The code in CloseWithError was written synchronously: it constructs the close capsule and attempts to write it to the stream.

However, in the QUIC layer, writing to a stream is a blocking operation if the flow control window is exhausted. If the peer (the attacker) decides to set the MAX_STREAM_DATA for that stream to exactly the number of bytes already received and not a single byte more, the sender is paralyzed. The Go runtime sees a blocking network call and parks the goroutine, waiting for a signal that the window has updated. Since the attacker has no intention of sending that update, the signal never comes. The goroutine enters a comatose state, never to wake up, yet still consuming stack memory and referencing the session context.

The Code: Blocking on the Exit Ramp

The smoking gun resides in how CloseWithError interacts with the underlying stream writer. While the exact pre-patch code is complex, the logic flowed roughly like this:

func (s *Session) CloseWithError(code SessionErrorCode, msg string) error {
    // ... logic to prepare close capsule ...
    
    // CRITICAL FLAW: This Write call blocks if flow control is 0.
    // There is no timeout here, and it relies on the peer.
    if err := s.sendCloseSessionCapsule(code, msg); err != nil {
        return err
    }
    
    // ... cleanup ...
    return nil
}

The fix, landing in commit 9d448b125754f4c83064afb2c586221214e55eec, fundamentally changes how stream closure and cancellation interact. The maintainers introduced smarter handling using the QUIC Stream Resets logic and decoupled the closure signaling from the strict flow control dependency.

Instead of blindly blocking, the patched version ensures that if the stream allows for partial delivery or if the write is blocked, the session tear-down can still proceed. It treats the WT_CLOSE_SESSION transmission as a "best effort" or at least one that shouldn't hold the application hostage. They effectively said: "If we can't tell them we're leaving, just leave."

The Exploit: The Passive-Aggressive Standoff

Exploiting this is trivially easy for anyone who can craft raw QUIC frames. You don't need a buffer overflow or a heap groom. You just need to be stubborn.

Here is the attack chain:

  1. Handshake: The attacker connects to the target WebTransport server using a modified QUIC client.
  2. Saturation: The attacker initiates a WebTransport session. The CONNECT stream is established.
  3. The Choke: The attacker reads the initial headers but then stops sending MAX_STREAM_DATA frames for the CONNECT stream. Effectively, the attacker tells the server: "my receive buffer is full."
  4. The Trigger: The attacker does something to trigger a session close (or waits for the server to time out via application logic). Alternatively, the attacker can just leave the connection idle.
  5. The Deadlock: The server decides to close the session. It calls CloseWithError. It tries to write the WT_CLOSE_SESSION capsule. The QUIC layer sees 0 credits. It blocks.

This diagram illustrates the deadlock:

Repeat this 10,000 times, and the server's scheduler collapses under the weight of thousands of dormant goroutines.

The Impact: Death by a Thousand Waits

This vulnerability is a classic asymmetric DoS. The attacker spends negligible resources—maintaining a few open UDP sockets is cheap. The server, however, pays a heavy price.

In Go, a goroutine is lightweight, starting at around 2KB of stack space. That sounds small. But associated with that goroutine is the Session object, buffers, and potentially application-level state attached to the context. If an attacker opens 50,000 connections and hangs them all, you aren't just losing 100MB of RAM. You are clogging the Go runtime scheduler and exhausting file descriptors (if the implementation uses them per connection, though QUIC is UDP, the internal maps grow).

This isn't just about crashing the server. It's about degrading it to the point of uselessness. Legitimate users will find the server unresponsive or extremely slow as the garbage collector struggles and the runtime manages an ever-growing list of blocked routines. It’s a silent, creeping death for the application.

The Fix: Learning to Let Go

The remediation is straightforward: Update to webtransport-go v0.10.0.

The patch effectively implements a "rude" shutdown when the polite one isn't possible. By enabling partial delivery and fixing the interaction between CancelWrite and Close, the library now correctly identifies when a stream is effectively dead or blocked and cleans up the resources without waiting for the peer's permission.

For developers unable to patch immediately (though you really should), mitigation is difficult. You could implement aggressive deadlines on the quic.Connection level, killing the entire UDP connection if it stalls, but that's a blunt instrument that might affect legitimate users on poor networks. The code fix is the only true solution.

Official Patches

quic-goRelease v0.10.0 fixing the vulnerability

Fix Analysis (1)

Technical Appendix

CVSS Score
5.3/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

Affected Systems

Go applications using `webtransport-go` < v0.10.0WebTransport servers based on quic-goHTTP/3 gateways terminating WebTransport sessions

Affected Versions Detail

Product
Affected Versions
Fixed Version
webtransport-go
quic-go
< 0.10.0v0.10.0
AttributeDetail
CWE IDCWE-400 (Uncontrolled Resource Consumption)
CVSS v3.15.3 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L)
Attack VectorNetwork (QUIC Stream Manipulation)
ImpactDenial of Service (Goroutine/Memory Leak)
StatusFixed in v0.10.0
Exploit MaturityProof of Concept (Trivial)

MITRE ATT&CK Mapping

T1499Endpoint Denial of Service
Impact
T1498Network Denial of Service
Impact
CWE-400
Uncontrolled Resource Consumption

The product does not properly control the allocation and maintenance of a limited resource, enabling an actor to influence the amount of resources consumed, eventually leading to exhaustion.

Known Exploits & Detection

TheoryExploitation involves a modified QUIC client that withholds MAX_STREAM_DATA frames.

Vulnerability Timeline

Vulnerability Published
2026-02-12
Patch Released (v0.10.0)
2026-02-12

References & Sources

  • [1]GitHub Advisory
  • [2]NVD Record

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.