Skip to content

4.0.4: resolveDNSHost rewrites the fetch URL to the resolved IP, breaking TLS (SNI/cert mismatch) for all HTTPS URLs #182

Description

@robe

Since 4.0.4, passing the resolveDNSHost option makes getLinkPreview fail for effectively every HTTPS URL.

The DNS-rebinding hardening in #181 introduced getValidatedFetchUrl, which rewrites the fetch URL's hostname to the IP returned by the callback (build/index.js:166, parsedUrl.hostname = formatHostnameForUrl(resolvedAddress)). The fetch then goes to https://<ip>/..., so the TLS handshake sends the bare IP as the SNI server name. SNI-routed hosts (any CDN) can't route the connection, and certificate validation fails because certs are issued for hostnames. The result is fetch failed with ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE.

The README's SSRF example resolves to the bare IP address, so anyone following the documented usage hits this. (Callbacks that return a full http(s):// URL bypass the rewrite and still work, but that's not what the docs show.)

To be clear, this report is about a functional regression in the hardening, not a challenge to the security intent of #181.

To Reproduce

const { getLinkPreview } = require('link-preview-js'); // 4.0.4
const dns = require('node:dns/promises');

getLinkPreview('https://example.com', {
  resolveDNSHost: async (url) => (await dns.lookup(new URL(url).hostname)).address,
}).then(console.log, console.error);
// 4.0.4: fetch failed (ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE)
// 4.0.3: resolves normally

The same call without resolveDNSHost succeeds on 4.0.4. Confirmed against multiple public HTTPS sites (example.com, guide.michelin.com).

Version with resolveDNSHost without
4.0.4 fails (TLS handshake) works
4.0.3 and earlier works works

Expected behavior

HTTPS previews succeed with resolveDNSHost set, as on <= 4.0.3. Through 4.0.3 the resolved IP was only validated (throwOnLoopback) and the fetch still used the original hostname URL.

Environment

  • Node.js (server), reproduced on Node 22
  • link-preview-js 4.0.4

Additional context

Suggested fix: keep the rebinding protection but pin at the connection layer instead of rewriting the URL. Resolve once, validate, then connect to the validated IP while keeping the original hostname for SNI and the Host header (e.g. an undici Agent with a custom lookup/connect, or got's dnsLookup hook).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions