High severity8.7NVD Advisory· Published May 11, 2026· Updated May 13, 2026
CVE-2026-43897
CVE-2026-43897
Description
Link Preview JS extracts web links information. Prior to 4.0.1, the library did not check for IPv6 loopback attacks. There was also a DNS attack, where an address could be resolved into an internal IP. This could cause internal data leaks. This vulnerability is fixed in 4.0.1.
Patches
14396d48909faMerge pull request #179 from OP-Engineering/oscar/loopback-fixes
4 files changed · +72 −3
constants.ts+17 −2 modified@@ -5,6 +5,9 @@ export const CONSTANTS = { "(?:(?:https?|ftp)://)" + // user:pass authentication "(?:\\S+(?::\\S*)?@)?" + + // block internal-only hostnames and wildcard DNS rebinding services + "(?![^/?#]+\\.(?:internal|local)(?::\\d{2,5})?(?:[/?#]|$))" + + "(?![^/?#]+\\.(?:nip|sslip)\\.io(?::\\d{2,5})?(?:[/?#]|$))" + "(?:" + // IP address exclusion // private & local networks @@ -34,7 +37,7 @@ export const CONSTANTS = { // resource path "(?:[/?#]\\S*)?" + "$", - "i" + "i", ), REGEX_LOOPBACK: new RegExp( @@ -63,8 +66,20 @@ export const CONSTANTS = { "|" + // Carrier-Grade NAT (CGNAT): 100.64.0.0 - 100.127.255.255 "(?:100\\.(?:6[4-9]|[7-9]\\d|1[0-1]\\d)(?:\\.\\d{1,3}){2})" + + "|" + + // IPv6 loopback + "(?:::1)" + + "|" + + // IPv4-mapped IPv6 loopback: ::ffff:127.0.0.0/104 + "(?:::ffff:127(?:\\.\\d{1,3}){3})" + + "|" + + // IPv6 Unique Local Address (ULA): fc00::/7 + "(?:f[c-d][0-9a-f]{2}:[0-9a-f:]+)" + + "|" + + // IPv6 link-local unicast: fe80::/10 + "(?:fe[89ab][0-9a-f]:[0-9a-f:]+)" + "$", - "i" + "i", ), REGEX_CONTENT_TYPE_IMAGE: new RegExp("image/.*", "i"),
index.ts+4 −0 modified@@ -413,6 +413,10 @@ export async function getLinkPreview(text: string, options?: ILinkPreviewOptions const resolvedUrl = await options.resolveDNSHost(detectedUrl); throwOnLoopback(resolvedUrl); + } else { + console.error( + "[link-preview-js] You are not resolving DNS addresses (resolveDNSHost option) before fetching a link. This can cause loopback attacks. Always try to resolve DNS addresses", + ); } const timeout = options?.timeout ?? 3000; // 3 second timeout default
__tests__/index.spec.ts+42 −0 modified@@ -1,6 +1,22 @@ import { getLinkPreview, getPreviewFromContent } from "../index"; +import { CONSTANTS } from "../constants"; import prefetchedResponse from "./sampleResponse.json"; +describe(`#REGEX_LOOPBACK`, () => { + it(`matches IPv6 loopback and local ranges`, () => { + expect(CONSTANTS.REGEX_LOOPBACK.test(`::1`)).toBe(true); + expect(CONSTANTS.REGEX_LOOPBACK.test(`::ffff:127.0.0.1`)).toBe(true); + expect(CONSTANTS.REGEX_LOOPBACK.test(`fc00::1`)).toBe(true); + expect(CONSTANTS.REGEX_LOOPBACK.test(`fd12:3456:789a::1`)).toBe(true); + expect(CONSTANTS.REGEX_LOOPBACK.test(`fe80::abcd`)).toBe(true); + expect(CONSTANTS.REGEX_LOOPBACK.test(`febf::abcd`)).toBe(true); + }); + + it(`does not match non-local IPv6 addresses`, () => { + expect(CONSTANTS.REGEX_LOOPBACK.test(`2001:4860:4860::8888`)).toBe(false); + }); +}); + describe(`#getLinkPreview()`, () => { it(`should extract link info from just URL`, async () => { const linkInfo: any = await getLinkPreview(`https://www.youtube.com/watch?v=wuClZjOdT30`, { @@ -150,6 +166,32 @@ describe(`#getLinkPreview()`, () => { ).rejects.toThrowErrorMatchingSnapshot(); }); + it(`should block .internal hostnames`, async () => { + await expect( + getLinkPreview( + `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token`, + ), + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + it(`should block .local hostnames`, async () => { + await expect( + getLinkPreview(`http://printer.local/status`), + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + it(`should block nip.io wildcard hostnames`, async () => { + await expect( + getLinkPreview(`http://169.254.169.254.nip.io/latest/meta-data/iam/security-credentials/`), + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + it(`should block sslip.io wildcard hostnames`, async () => { + await expect( + getLinkPreview(`http://127.0.0.1.sslip.io/`), + ).rejects.toThrowErrorMatchingSnapshot(); + }); + it(`should handle empty strings gracefully`, async () => { await expect(getLinkPreview(``)).rejects.toThrowErrorMatchingSnapshot(); });
__tests__/__snapshots__/index.spec.ts.snap+9 −1 modified@@ -1,7 +1,15 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`#getLinkPreview() no link in text should fail gracefully 1`] = `"link-preview-js did not receive a valid a url or text"`; +exports[`#getLinkPreview() should block .internal hostnames 1`] = `"link-preview-js did not receive a valid a url or text"`; + +exports[`#getLinkPreview() should block .local hostnames 1`] = `"link-preview-js did not receive a valid a url or text"`; + +exports[`#getLinkPreview() should block nip.io wildcard hostnames 1`] = `"link-preview-js did not receive a valid a url or text"`; + +exports[`#getLinkPreview() should block sslip.io wildcard hostnames 1`] = `"link-preview-js did not receive a valid a url or text"`; + exports[`#getLinkPreview() should handle empty strings gracefully 1`] = `"link-preview-js did not receive a valid url or text"`; exports[`#getLinkPreview() should handle malformed urls gracefully 1`] = `"link-preview-js did not receive a valid a url or text"`;
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
6- github.com/advisories/GHSA-4gp8-rjrq-ch6qghsaADVISORY
- github.com/OP-Engineering/link-preview-js/commit/4396d48909fab37553c0e93e26447fe218363edenvd
- github.com/OP-Engineering/link-preview-js/pull/179nvd
- github.com/OP-Engineering/link-preview-js/releases/tag/4.0.1nvd
- github.com/OP-Engineering/link-preview-js/security/advisories/GHSA-4gp8-rjrq-ch6qnvd
- nvd.nist.gov/vuln/detail/CVE-2026-43897ghsa
News mentions
0No linked articles in our index yet.