Feb 12, 2026·6 min read·42 visits
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.
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 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.
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.
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 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.
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.
RST_STREAM).From the perspective of the rate limiter:
From the perspective of the server's RAM:
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 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 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.
go get github.com/quic-go/webtransport-go@v0.10.0github.com/quic-go/quic-go is updated to at least v0.59.0.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.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
webtransport-go quic-go | < 0.10.0 | 0.10.0 |
| Attribute | Detail |
|---|---|
| CWE | CWE-401 (Memory Leak) |
| CVSS | 5.3 (Medium) |
| Attack Vector | Network |
| Exploit Status | Trivial / High Likelihood |
| Impact | Denial of Service (OOM) |
| Privileges | None |
Missing Release of Memory after Effective Lifetime
CVE-2026-107387 is a high-impact uncontrolled memory allocation vulnerability in music-metadata, a widely used Node.js metadata parser. The flaw occurs in the APEv2 tag parser, where the library reads an attacker-controlled 32-bit integer indicating the tag size and immediately requests a corresponding heap buffer reservation. Because this allocation occurs before validating if the input stream actually contains those bytes, an attacker can supply a minuscule audio file to trigger large, disproportionate allocations, resulting in heap exhaustion and an uncatchable process-wide Out of Memory (OOM) crash.
An input validation vulnerability exists in music-metadata versions prior to 11.16.0, where parsing a crafted MP4 file containing a sample-description (stsd) box with a zero-value size entry causes a synchronous infinite loop and memory exhaustion, resulting in complete Denial of Service.
A path traversal vulnerability in datamodel-code-generator allows remote attackers to write or overwrite arbitrary files on the local host filesystem via a manipulated Protobuf schema containing malicious weak import paths.
PraisonAI is vulnerable to an arbitrary local file read vulnerability prior to version 4.6.78. The flaw is in the ContextGatherer component, where validation checks are executed only after files are parsed and appended to the context bundle, bypassing security constraints.
An algorithmic complexity vulnerability (CWE-770) in the Excelize library allows remote attackers to cause resource exhaustion (100% CPU usage) via a crafted Microsoft Excel spreadsheet. This occurs because the look-ahead row index parsing in Rows.Columns() fails to enforce upper boundary limits, enabling an out-of-bounds row index to trigger an infinite seek loop inside the Rows iterator.
An unauthenticated Server-Side Request Forgery (SSRF) vulnerability exists in Ghost CMS from version 6.54.1 to 6.65.0. The vulnerability stems from a validation bypass in the favicon resolution logic within the bookmark-fetching subsystem, which allows remote, unauthenticated attackers to trigger arbitrary HTTP requests to the local host and internal networks. This bypass circumvents the custom DNS-level IP blocklist controls configured globally in the application.