From ad791f8ea5baaef062d13e5aeabf9a3a0e61923e Mon Sep 17 00:00:00 2001 From: Keon Date: Sat, 28 Mar 2026 09:21:58 -0400 Subject: [PATCH] fix: block localhost and loopback addresses in SSRF protection (#28622) * fix: block localhost and loopback addresses in SSRF protection Co-Authored-By: Claude Opus 4.6 (1M context) * fix: block loopback IPs by hostname in SSRF protection Add 127.0.0.1, ::1, [::1], and 0.0.0.0 to blocked hostnames list for defense-in-depth protection against SSRF attacks targeting localhost. Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Romit Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com> --- packages/lib/ssrfProtection.test.ts | 21 +++++++++++++-------- packages/lib/ssrfProtection.ts | 6 ++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/lib/ssrfProtection.test.ts b/packages/lib/ssrfProtection.test.ts index d65dfd5c81..685bd71452 100644 --- a/packages/lib/ssrfProtection.test.ts +++ b/packages/lib/ssrfProtection.test.ts @@ -57,12 +57,16 @@ describe("isPrivateIP", () => { describe("isBlockedHostname", () => { it.each([ - "localhost", // loopback hostname - "169.254.169.254", // AWS/Azure/DigitalOcean - "metadata.google.internal", // GCP - "169.254.169.254.", // trailing dot normalization - "METADATA.GOOGLE.INTERNAL", // case insensitive - ])("blocks cloud metadata endpoint %s", (hostname) => { + "localhost", + "127.0.0.1", + "::1", + "[::1]", + "0.0.0.0", + "169.254.169.254", + "metadata.google.internal", + "169.254.169.254.", + "METADATA.GOOGLE.INTERNAL", + ])("blocks %s", (hostname) => { expect(isBlockedHostname(hostname)).toBe(true); }); @@ -104,7 +108,8 @@ describe("validateUrlForSSRFSync", () => { ["http://example.com/logo.png", "Only HTTPS URLs are allowed"], ["ftp://example.com/file", "Only HTTPS URLs are allowed"], ["data:text/html,", "Non-image data URL"], - ["https://127.0.0.1/logo.png", "Private IP address"], + ["https://127.0.0.1/logo.png", "Blocked hostname"], + ["https://0.0.0.0/logo.png", "Blocked hostname"], ["https://169.254.169.254/latest/meta-data/", "Blocked hostname"], ["https://localhost/logo.png", "Blocked hostname"], ["not-a-url", "Invalid URL format"], @@ -114,7 +119,7 @@ describe("validateUrlForSSRFSync", () => { }); it.each([ - ["https://[::1]/", "Private IP address"], + ["https://[::1]/", "Blocked hostname"], ["https://[fe80::1]/path", "Private IP address"], ["https://[fc00::1]:8080/", "Private IP address"], ["https://[::ffff:127.0.0.1]/", "Private IP address"], diff --git a/packages/lib/ssrfProtection.ts b/packages/lib/ssrfProtection.ts index 242cfebfaa..1333d6b984 100644 --- a/packages/lib/ssrfProtection.ts +++ b/packages/lib/ssrfProtection.ts @@ -31,8 +31,10 @@ const CLOUD_METADATA_ENDPOINTS: string[] = [ "metadata.google.com", // GCP alternate ]; -// Hostnames blocked on Cal.com SaaS (includes metadata + localhost) -const BLOCKED_HOSTNAMES: string[] = [...CLOUD_METADATA_ENDPOINTS, "localhost"]; +const LOOPBACK_HOSTNAMES: string[] = ["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]; + +// Hostnames blocked on Cal.com SaaS (includes metadata + loopback) +const BLOCKED_HOSTNAMES: string[] = [...CLOUD_METADATA_ENDPOINTS, ...LOOPBACK_HOSTNAMES]; const CAL_AVATAR_PATH_REGEX = /^\/api\/avatar\/.+\.png$/;