Deno's Node.js Compatibility Runtime has Cross-Session Data Contamination
Description
Deno is a JavaScript, TypeScript, and WebAssembly runtime. Starting in version 1.35.1 and prior to version 1.36.3, a vulnerability in Deno's Node.js compatibility runtime allows for cross-session data contamination during simultaneous asynchronous reads from Node.js streams sourced from sockets or files. The issue arises from the re-use of a global buffer (BUF) in stream_wrap.ts used as a performance optimization to limit allocations during these asynchronous read operations. This can lead to data intended for one session being received by another session, potentially resulting in data corruption and unexpected behavior. This affects all users of Deno that use the node.js compatibility layer for network communication or other streams, including packages that may require node.js libraries indirectly. Version 1.36.3 contains a patch for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
denocrates.io | >= 1.35.1, < 1.36.3 | 1.36.3 |
Affected products
1Patches
13e9fb8aafd98fix(ext/node): simultaneous reads can leak into each other (#20223)
2 files changed · +105 −40
cli/tests/unit_node/net_test.ts+57 −0 modified@@ -130,3 +130,60 @@ Deno.test("[node/net] connection event has socket value", async () => { await Promise.all([p, p2]); }); + +// https://github.com/denoland/deno/issues/20188 +Deno.test("[node/net] multiple Sockets should get correct server data", async () => { + const p = deferred(); + const p2 = deferred(); + + const dataReceived1 = deferred(); + const dataReceived2 = deferred(); + + const events1: string[] = []; + const events2: string[] = []; + + const server = net.createServer(); + server.on("connection", (socket) => { + assert(socket !== undefined); + socket.on("data", (data) => { + socket.write(new TextDecoder().decode(data)); + }); + }); + + server.listen(async () => { + // deno-lint-ignore no-explicit-any + const { port } = server.address() as any; + + const socket1 = net.createConnection(port); + const socket2 = net.createConnection(port); + + socket1.on("data", (data) => { + events1.push(new TextDecoder().decode(data)); + dataReceived1.resolve(); + }); + + socket2.on("data", (data) => { + events2.push(new TextDecoder().decode(data)); + dataReceived2.resolve(); + }); + + socket1.write("111"); + socket2.write("222"); + + await Promise.all([dataReceived1, dataReceived2]); + + socket1.end(); + socket2.end(); + + server.close(() => { + p.resolve(); + }); + + p2.resolve(); + }); + + await Promise.all([p, p2]); + + assertEquals(events1, ["111"]); + assertEquals(events2, ["222"]); +});
ext/node/polyfills/internal_binding/stream_wrap.ts+48 −40 modified@@ -311,56 +311,61 @@ export class LibuvStreamWrap extends HandleWrap { /** Internal method for reading from the attached stream. */ async #read() { - let buf = BUF; - - let nread: number | null; - const ridBefore = this[kStreamBaseField]!.rid; + const isOwnedBuf = bufLocked; + let buf = bufLocked ? new Uint8Array(SUGGESTED_SIZE) : BUF; + bufLocked = true; try { - nread = await this[kStreamBaseField]!.read(buf); - } catch (e) { - // Try to read again if the underlying stream resource - // changed. This can happen during TLS upgrades (eg. STARTTLS) - if (ridBefore != this[kStreamBaseField]!.rid) { - return this.#read(); - } + let nread: number | null; + const ridBefore = this[kStreamBaseField]!.rid; + try { + nread = await this[kStreamBaseField]!.read(buf); + } catch (e) { + // Try to read again if the underlying stream resource + // changed. This can happen during TLS upgrades (eg. STARTTLS) + if (ridBefore != this[kStreamBaseField]!.rid) { + return this.#read(); + } - if ( - e instanceof Deno.errors.Interrupted || - e instanceof Deno.errors.BadResource - ) { - nread = codeMap.get("EOF")!; - } else if ( - e instanceof Deno.errors.ConnectionReset || - e instanceof Deno.errors.ConnectionAborted - ) { - nread = codeMap.get("ECONNRESET")!; - } else { - nread = codeMap.get("UNKNOWN")!; - } + if ( + e instanceof Deno.errors.Interrupted || + e instanceof Deno.errors.BadResource + ) { + nread = codeMap.get("EOF")!; + } else if ( + e instanceof Deno.errors.ConnectionReset || + e instanceof Deno.errors.ConnectionAborted + ) { + nread = codeMap.get("ECONNRESET")!; + } else { + nread = codeMap.get("UNKNOWN")!; + } - buf = new Uint8Array(0); - } + buf = new Uint8Array(0); + } - nread ??= codeMap.get("EOF")!; + nread ??= codeMap.get("EOF")!; - streamBaseState[kReadBytesOrError] = nread; + streamBaseState[kReadBytesOrError] = nread; - if (nread > 0) { - this.bytesRead += nread; - } + if (nread > 0) { + this.bytesRead += nread; + } - buf = buf.slice(0, nread); + buf = isOwnedBuf ? buf.subarray(0, nread) : buf.slice(0, nread); - streamBaseState[kArrayBufferOffset] = 0; + streamBaseState[kArrayBufferOffset] = 0; - try { - this.onread!(buf, nread); - } catch { - // swallow callback errors. - } + try { + this.onread!(buf, nread); + } catch { + // swallow callback errors. + } - if (nread >= 0 && this.#reading) { - this.#read(); + if (nread >= 0 && this.#reading) { + this.#read(); + } + } finally { + bufLocked = false; } } @@ -423,4 +428,7 @@ export class LibuvStreamWrap extends HandleWrap { } } +// Used in #read above const BUF = new Uint8Array(SUGGESTED_SIZE); +// We need to ensure that only one inflight read request uses the cached buffer above +let bufLocked = false;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-wrqv-pf6j-mqjpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-27935ghsaADVISORY
- github.com/denoland/deno/commit/3e9fb8aafd9834ebacd27734cea4310caaf794c6ghsax_refsource_MISCWEB
- github.com/denoland/deno/issues/20188ghsax_refsource_MISCWEB
- github.com/denoland/deno/security/advisories/GHSA-wrqv-pf6j-mqjpghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.