From c15868054dc08d6b12ec66cef6047282cb44381d Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Sat, 25 Jul 2026 16:55:13 -0600 Subject: [PATCH] fix(url-guard): block root-dotted internal hosts --- apps/web/lib/url-guard.test.ts | 5 +++++ apps/web/lib/url-guard.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/lib/url-guard.test.ts b/apps/web/lib/url-guard.test.ts index 8d3fda4..d57a015 100644 --- a/apps/web/lib/url-guard.test.ts +++ b/apps/web/lib/url-guard.test.ts @@ -8,6 +8,11 @@ describe("validateExternalUrl (SSRF guard)", () => { it("blocks localhost", () => { expect(validateExternalUrl("http://localhost/x").ok).toBe(false); }); + it("blocks internal hostnames with DNS root dots", () => { + expect(validateExternalUrl("http://localhost./x").ok).toBe(false); + expect(validateExternalUrl("http://app.localhost./x").ok).toBe(false); + expect(validateExternalUrl("http://metadata.google.internal./x").ok).toBe(false); + }); it("blocks private 10.x", () => { expect(validateExternalUrl("http://10.0.0.5/x").ok).toBe(false); }); diff --git a/apps/web/lib/url-guard.ts b/apps/web/lib/url-guard.ts index 7490554..7f4c973 100644 --- a/apps/web/lib/url-guard.ts +++ b/apps/web/lib/url-guard.ts @@ -15,6 +15,7 @@ function isBlockedHost(host: string): boolean { const ipv4 = ipv4FromMappedIPv6(host) ?? host; return ( ipv4 === "localhost" || + ipv4.endsWith(".localhost") || ipv4 === "0.0.0.0" || ipv4 === "::1" || ipv4 === "::" || @@ -42,7 +43,7 @@ export function validateExternalUrl(raw: string): { ok: true; url: URL } | { ok: if (url.protocol !== "http:" && url.protocol !== "https:") { return { ok: false, error: "Only http and https URLs are allowed." }; } - const host = url.hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1"); + const host = url.hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1").replace(/\.+$/, ""); if (isBlockedHost(host)) { return { ok: false, error: "That host is not allowed." }; }