From 071d9cd106ba8bcd97a068468612cd97108cd5ce Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:08:02 +0000 Subject: [PATCH 1/4] fix(security): harden web SSRF validation for non-canonical IPs and metadata hostnames - Add `normalizeHost` in `web/lib/validation.ts` to convert integer, hex, and octal/dotted IPv4 notations into canonical dotted-decimal strings before IP range checking via `ipaddr.js`. - Expand `isBlockedInternalHostname` with `BLOCKED_HOSTNAMES` to block cloud metadata endpoints (`metadata.google.internal`, `metadata.azure.com`, `169.254.169.254`, etc.) and handle trailing dots on domain names. - Add unit tests in `web/tests/ssrf.test.ts` covering non-canonical IP formats and cloud/container metadata hostnames. Co-authored-by: d-oit <6849456+d-oit@users.noreply.github.com> --- web/lib/validation.ts | 78 ++++++++++++++++++++++++++++++++++++++++-- web/package-lock.json | 4 +-- web/tests/ssrf.test.ts | 17 +++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/web/lib/validation.ts b/web/lib/validation.ts index 5407788f..39323e00 100644 --- a/web/lib/validation.ts +++ b/web/lib/validation.ts @@ -6,12 +6,82 @@ import dns from "node:dns/promises"; const MAX_QUERY_LENGTH = 10000; const MAX_URL_LENGTH = 2048; +/** + * Normalize host string by converting integer, hex, or octal IPv4 formats + * to canonical dotted-decimal IPv4 address strings if applicable. + */ +function normalizeHost(hostname: string): string { + const h = hostname.trim().toLowerCase(); + + // Decimal integer notation (e.g., 2130706433 -> 127.0.0.1) + if (/^\d+$/.test(h)) { + const num = Number(h); + if (num >= 0 && num <= 0xffffffff) { + return [ + (num >>> 24) & 0xff, + (num >>> 16) & 0xff, + (num >>> 8) & 0xff, + num & 0xff, + ].join("."); + } + } + + // Hexadecimal notation (e.g., 0x7f000001 -> 127.0.0.1) + if (/^0x[0-9a-f]+$/i.test(h)) { + const num = parseInt(h, 16); + if (num >= 0 && num <= 0xffffffff) { + return [ + (num >>> 24) & 0xff, + (num >>> 16) & 0xff, + (num >>> 8) & 0xff, + num & 0xff, + ].join("."); + } + } + + // Dotted notation with octal/hex/decimal parts (e.g., 0177.0.0.1, 0x7f.0.0.1) + const parts = h.split("."); + if (parts.length === 4) { + const convertedParts: number[] = []; + for (const part of parts) { + if (/^0x[0-9a-f]+$/i.test(part)) { + convertedParts.push(parseInt(part, 16)); + } else if (/^0[0-7]+$/.test(part)) { + convertedParts.push(parseInt(part, 8)); + } else if (/^\d+$/.test(part)) { + convertedParts.push(parseInt(part, 10)); + } else { + return h; + } + } + if (convertedParts.every((p) => p >= 0 && p <= 255)) { + return convertedParts.join("."); + } + } + + return h; +} + +const BLOCKED_HOSTNAMES = new Set([ + "localhost", + "metadata.google.internal", + "metadata.google", + "metadata.azure.com", + "169.254.169.254", + "kubernetes.default.svc", + "kubernetes.default", + "kubernetes", + "host.docker.internal", + "gateway.docker.internal", +]); + /** * Check if an IP address is private or reserved */ function isPrivateIpAddress(ip: string): boolean { try { - const addr = ipaddr.parse(ip); + const normalizedIp = normalizeHost(ip); + const addr = ipaddr.parse(normalizedIp); const kind = addr.kind(); // Handle IPv4-mapped IPv6 and similar encapsulations @@ -64,9 +134,11 @@ function isPrivateIpAddress(ip: string): boolean { } function isBlockedInternalHostname(hostname: string): boolean { - const normalized = hostname.toLowerCase(); + const normalized = hostname.toLowerCase().replace(/\.$/, ""); + if (BLOCKED_HOSTNAMES.has(normalized)) { + return true; + } return ( - normalized === "localhost" || normalized.endsWith(".local") || normalized.endsWith(".internal") ); diff --git a/web/package-lock.json b/web/package-lock.json index 85ecaca9..9cb5060c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "do-web-doc-resolver-ui", - "version": "0.3.9", + "version": "0.3.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "do-web-doc-resolver-ui", - "version": "0.3.9", + "version": "0.3.10", "dependencies": { "@vercel/analytics": "^2.0.1", "@vercel/speed-insights": "^2.0.0", diff --git a/web/tests/ssrf.test.ts b/web/tests/ssrf.test.ts index 43153498..13c3ab76 100644 --- a/web/tests/ssrf.test.ts +++ b/web/tests/ssrf.test.ts @@ -60,4 +60,21 @@ describe("SSRF Validation", () => { expect(validateUrl("http://[::FFFF:127.0.0.1]").valid).toBe(false); expect(validateUrl("http://LOCALHOST").valid).toBe(false); }); + + it("rejects decimal integer, hex, octal IP notations and cloud metadata hostnames", () => { + const dangerousUrls = [ + "http://2130706433/test", // 127.0.0.1 in decimal integer + "http://0x7f000001/test", // 127.0.0.1 in hex + "http://0177.0.0.1/test", // 127.0.0.1 in octal prefix + "http://169.254.169.254/latest/meta-data/", + "http://metadata.google.internal/computeMetadata/v1/", + "http://metadata.google/", + "http://metadata.azure.com/", + "http://kubernetes.default.svc/", + "http://host.docker.internal/", + ]; + for (const url of dangerousUrls) { + expect(validateUrl(url).valid).toBe(false); + } + }); }); From 7ef0b7dadc618b46703c37c01de49c65e134866d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:12:28 +0000 Subject: [PATCH 2/4] fix(security): harden web SSRF validation for non-canonical IPs and metadata hostnames - Add `normalizeHost` in `web/lib/validation.ts` to convert integer, hex, and octal/dotted IPv4 notations into canonical dotted-decimal strings before IP range checking via `ipaddr.js`. - Expand `isBlockedInternalHostname` with `BLOCKED_HOSTNAMES` to block cloud metadata endpoints (`metadata.google.internal`, `metadata.azure.com`, `169.254.169.254`, etc.) and handle trailing dots on domain names. - Add unit tests in `web/tests/ssrf.test.ts` covering non-canonical IP formats and cloud/container metadata hostnames. Co-authored-by: d-oit <6849456+d-oit@users.noreply.github.com> From bf7f8316025814b25816df381e3b53d3ec51d517 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:17:52 +0000 Subject: [PATCH 3/4] fix(security): harden web SSRF validation for non-canonical IPs and metadata hostnames Co-authored-by: d-oit <6849456+d-oit@users.noreply.github.com> From 0fddc7bebe87903a6ffd61cf04102acc7ff1d71a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:23:26 +0000 Subject: [PATCH 4/4] fix(security): harden web SSRF validation for non-canonical IPs Co-authored-by: d-oit <6849456+d-oit@users.noreply.github.com>