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-21438
5.3

The Zombie Stream Apocalypse: Analyzing CVE-2026-21438 in webtransport-go

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 12, 2026·6 min read·7 visits

No Known Exploit

Executive Summary (TL;DR)

The `webtransport-go` library prior to v0.10.0 fails to delete stream references from an internal map after they are closed. Attackers can open and immediately close millions of streams, bloating the server's memory until it crashes (OOM), even while staying within 'active stream' limits.

A comprehensive analysis of a memory exhaustion vulnerability in the `webtransport-go` library. By failing to remove closed streams from an internal tracking map, the library allows attackers to trigger a Denial of Service (DoS) via unlimited memory consumption, bypassing standard concurrency limits.

The Hook: WebTransport's Dirty Secret

WebTransport is the new hotness in the protocol world. Built on top of HTTP/3 and QUIC, it promises the low-latency, bidirectional communication that WebSockets could only dream of, but without the Head-of-Line blocking nightmares. It's complex, stateful, and fast. But as any systems programmer knows, 'stateful' is just a polite word for 'memory management minefield'.

When you build a protocol implementation in Go, you often lull yourself into a false sense of security. 'I have a Garbage Collector,' you whisper to yourself as you sleep soundly at night. 'I don't need to free() anything.' And technically, you're right. You don't need to manually free memory.

But the Garbage Collector is not a psychic. It cannot clean up objects that you are still referencing. If you put a sticky note on a box in the attic, the cleaner isn't going to throw it out. CVE-2026-21438 is exactly that: a digital hoarder's paradise where webtransport-go kept a reference to every single stream that ever existed in a session, long after those streams were dead and buried.

The Flaw: A Map That Only Grows

The vulnerability (CWE-401/CWE-459) is a classic case of unbounded data structure growth. In the affected versions of webtransport-go, the library maintains an internal map to track active WebTransport streams. This is standard practice; you need to map IDs to stream contexts to handle incoming data frames or state changes.

Here is the logic flaw: When a stream was initialized, it was eagerly added to this map. However, the developers missed the critical inverse operation. When a stream was closed—either by the local application or by a remote RESET_STREAM frame—the entry in the map was not removed.

This creates a fascinating discrepancy between the logical state of the connection and the physical memory usage. You could have a session with zero active streams, yet the underlying map structure could contain millions of entries pointing to dead stream contexts. The Go runtime sees these map entries as 'live' data, so the Garbage Collector (GC) kindly keeps its hands off. The server slowly suffocates, holding onto the ghosts of streams past.

The Code: Anatomy of a Leak

Let's look at the pattern that causes this. While I won't bore you with the entire 5,000-line diff, the core issue boils down to lifecycle symmetry. In Go, maps don't shrink automatically, and entries don't disappear unless you explicitly delete() them.

The Vulnerable Pattern

In the vulnerable versions, the session management looked something like this:

// Simplified logic of the vulnerable code
type Session struct {
    streams map[quic.StreamID]*Stream
    // ... other fields
}
 
func (s *Session) acceptStream() (*Stream, error) {
    // 1. Accept the QUIC stream
    qStr, err := s.conn.AcceptStream(context.Background())
    if err != nil { return nil, err }
 
    // 2. Wrap it and ADD to the map
    str := newStream(qStr)
    s.streams[str.StreamID()] = str 
 
    return str, nil
}
 
// AND THAT'S IT. There was no corresponding delete() 
// in the Close() path of the stream.

The Fix (v0.10.0)

The fix is elegantly simple. The maintainers introduced a cleanup mechanism that triggers when a stream dies. They essentially taught the library how to let go.

func (s *Session) removeStream(id quic.StreamID) {
    s.mx.Lock()
    defer s.mx.Unlock()
    delete(s.streams, id) // <--- The line that saves your RAM
}

This function is now hooked into the stream's closure lifecycle. When a stream is finished, it calls home to the parent Session and removes itself from the registry. It's basic housekeeping, but in high-throughput network services, housekeeping is critical infrastructure.

The Exploit: Death by a Thousand Streams

Exploiting this is trivially easy and frustratingly effective. Most DoS protections rely on concurrency limits. For example, a server might say, "You can only have 100 streams open at once." If you try to open stream #101, the server tells you to get lost.

But this bug doesn't require concurrent streams. It effectively leaks historical streams. An attacker can respect the concurrency limit perfectly and still crash the server.

The Attack Chain

  1. Handshake: Establish a single, valid WebTransport session.
  2. Churn: Open a stream. Immediately close it (send RST_STREAM).
  3. Repeat: Do this in a tight loop. Open, close, open, close.

From the perspective of the rate limiter:

  • Active Streams: 1
  • Limit: 100
  • Status: OK

From the perspective of the server's RAM:

  • Map Entries: 1, 2, ..., 1,000,000, ..., 50,000,000
  • Status: OOM Panic

The beauty of this attack is its stealth. It doesn't trigger "Max Open Files" warnings. It doesn't trip standard firewall concurrency rules. It just slowly eats memory until the Linux OOM killer walks in and shoots the process in the head.

The Impact: Why Should We Panic?

The impact here is purely Availability (the 'A' in CIA triad), but don't let the 'Low' severity score fool you. In a microservices environment, or a gateway handling WebTransport traffic for thousands of users, this is a distinct single-point-of-failure.

Because the attack vector is network-based and requires no authentication (if the endpoint is public) or low privileges, any script kiddie with a loop can take down a production service. The memory consumption is linear. If a Stream struct and its map overhead cost roughly 200 bytes, sending 5 million streams (which takes seconds on a fast connection) consumes 1GB of RAM. Do that from 10 different IPs, and you're eating server capacity faster than an auto-scaling group can spin up new nodes.

If you are using webtransport-go for gaming servers, real-time collaboration tools, or media ingestion, this is a "wake up at 3 AM" kind of bug.

The Fix: Learning to Let Go

The mitigation is straightforward: Update your dependencies. The fix landed in v0.10.0. This version introduces the necessary cleanup logic.

> [!WARNING] > Dependency Hell Alert: Version v0.10.0 of webtransport-go requires quic-go version v0.59.0 or later. This is a breaking API change for quic-go. You cannot just bump the patch version; you will likely need to refactor some of your QUIC handling code to match the new API surface.

Remediation Steps

  1. Update go.mod:
    go get github.com/quic-go/webtransport-go@v0.10.0
  2. Verify quic-go version: Ensure github.com/quic-go/quic-go is updated to at least v0.59.0.
  3. Audit your Wrappers: If you wrote custom wrappers around AcceptStream, ensure you aren't holding onto references there either. The library fix only clears the library's internal map. If you put the stream in a global slice, that's on you.

Official Patches

quic-goRelease notes for v0.10.0 containing the fix

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

WebTransport Servers using quic-go/webtransport-go < 0.10.0Go applications implementing HTTP/3 over QUIC with WebTransport support

Affected Versions Detail

Product
Affected Versions
Fixed Version
webtransport-go
quic-go
< 0.10.00.10.0
AttributeDetail
CWECWE-401 (Memory Leak)
CVSS5.3 (Medium)
Attack VectorNetwork
Exploit StatusTrivial / High Likelihood
ImpactDenial of Service (OOM)
PrivilegesNone

MITRE ATT&CK Mapping

T1499.003Endpoint Denial of Service: Application or System Exploitation
Impact
CWE-401
Improper Cleanup

Missing Release of Memory after Effective Lifetime

Known Exploits & Detection

Internal AnalysisExploitation is trivial via repeated stream creation and reset within a single session.

Vulnerability Timeline

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

References & Sources

  • [1]GHSA-2f2x-8mwp-p2gc
  • [2]CVE-2026-21438 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.