From a84273570e16f3dea1096c08981aa4d7e6b7b040 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Sat, 25 Jul 2026 16:45:45 -0600 Subject: [PATCH] fix(webhooks): block root-dotted internal hosts --- lib/url-guard.ts | 2 +- tests/url-guard.test.mjs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/url-guard.ts b/lib/url-guard.ts index eade374..cbc6488 100644 --- a/lib/url-guard.ts +++ b/lib/url-guard.ts @@ -8,7 +8,7 @@ export function isInternalUrl(raw: string): boolean { // Reject non-numeric ports; odd ports can hide alternate protocols. if (u.port !== "" && !/^\d{1,5}$/.test(u.port)) return true; - const h = u.hostname.toLowerCase(); + const h = u.hostname.toLowerCase().replace(/\.+$/, ""); if (h === "localhost" || h.endsWith(".localhost") || h === "metadata.google.internal") return true; if (h === "0.0.0.0") return true; diff --git a/tests/url-guard.test.mjs b/tests/url-guard.test.mjs index f00aa7b..cdde812 100644 --- a/tests/url-guard.test.mjs +++ b/tests/url-guard.test.mjs @@ -19,6 +19,12 @@ test("SSRF guard blocks internal IPv4 special-use ranges", () => { assert.equal(isInternalUrl("http://198.19.255.254/hook"), true); }); +test("SSRF guard blocks internal hostnames with DNS root dots", () => { + assert.equal(isInternalUrl("http://localhost./hook"), true); + assert.equal(isInternalUrl("http://app.localhost./hook"), true); + assert.equal(isInternalUrl("http://metadata.google.internal./hook"), true); +}); + test("SSRF guard allows public IPv6 webhook targets", () => { assert.equal(isInternalUrl("https://[2606:4700:4700::1111]/hook"), false); assert.equal(isInternalUrl("https://[::ffff:8.8.8.8]/hook"), false);