high · 7.5CVE-2026-61704Sep 2, 2026

CVE-2026-61704: link-preview-js DNS Rebinding SSRF Bypass

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

link-preview-js versions up to 4.0.3 let an attacker bypass the documented SSRF protection by controlling DNS so the first resolution returns a public IP (passing the check) and the second returns an…

Packagelink-preview-js
Ecosystemnpm
Affected<= 4.0.3
Fixed in4.0.4
CVE-2026-61704: link-preview-js DNS Rebinding SSRF Bypass

The problem

The library's resolveDNSHost option resolves and validates a hostname once, but then calls the native fetch() against the original hostname. The OS re-resolves the name at connect time, creating a TOCTOU window.

An attacker who controls the authoritative DNS for a submitted hostname can serve a public IP during validation and a loopback or internal IP during the real connection. This defeats the only documented SSRF mitigation and allows server-side fetches to reach 127.0.0.1, private RFC-1918 ranges, or cloud metadata endpoints (e.g., 169.254.169.254).

Proof of concept

A working proof-of-concept for CVE-2026-61704 in link-preview-js, with the exact payload below.

javascript
// Attacker controls DNS for make-PUB-rebind-127.0.0.1-rr.1u.ms
// First resolution → PUB IP (passes resolveDNSHost check)
// Second resolution → 127.0.0.1 (used by fetch())

import { getLinkPreview } from 'link-preview-js';

await getLinkPreview(
  'http://make-1.2.3.4-rebind-127.0.0.1-rr.1u.ms/',
  {
    resolveDNSHost: async (url) => {
      const { hostname } = new URL(url);
      const { resolve4 } = await import('dns/promises');
      const [ip] = await resolve4(hostname);
      return ip; // returns '1.2.3.4' (public) — check passes
      // fetch() re-resolves independently → gets 127.0.0.1
    },
  }
);

The root cause is a classic TOCTOU: validate one result, act on a second. The resolveDNSHost callback returns an IP that the library checks against an internal-address denylist, but the validated IP is then discarded. The library proceeds to call fetch(url) with the original hostname, so the Node.js HTTP stack re-resolves it independently.

The patch (PR #181) eliminates the window by adding undici as a dependency and creating a pinned dispatcher that dials the pre-validated IP at the TCP layer rather than letting the HTTP client re-resolve. The fix also replaces regex-based private-address checks with node:net BlockList checks and adds IPv6 embedded-IPv4 normalization, closing a secondary bypass path.

CWE-918 (SSRF) via CWE-362 (TOCTOU race).

The fix

Upgrade to link-preview-js 4.0.4. The patched version uses an undici pinned dispatcher so the TCP connection is forced to the already-validated IP, removing the re-resolution window entirely. No config change is needed, resolveDNSHost works correctly in 4.0.4.

Reported by ahmet-sahiner.

References: [1][2][3][4][5][6]

Related research