From 773436eb8408149150319a65621baf3a91cc368f Mon Sep 17 00:00:00 2001 From: RelunSec Date: Mon, 27 Apr 2026 17:07:49 -0400 Subject: [PATCH] Enhance IP checks and hostname parsing Added checks for IPv4-mapped and IPv4-compatible addresses in is_ip_internal function. Updated hostname parsing in is_url_safe_debug to remove brackets. --- src/helpers.ts | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index e3ead46..1e4d1e8 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -2,6 +2,7 @@ import * as ipaddr from "ipaddr.js"; import CIDR from "ip-cidr"; +import * as net from "node:net"; // Added for net.isIPv6 check const got = require("got").default; const DEBUG = false; const DSSRF_MAKE_REQUEST = process.env.DSSRF_MAKE_REQUEST; @@ -232,11 +233,32 @@ export function is_ip_internal(ip: string): boolean { if (parsed.kind() === "ipv6") { const range = parsed.range(); - return ( - range === "loopback" || - range === "linkLocal" || - range === "uniqueLocal" - ); + if (range !== "unicast") { + // Check for IPv4-mapped or IPv4-compatible addresses + if (range === "ipv4Mapped" || range === "rfc6145") { + try { + // @ts-ignore + const ipv4 = parsed.toIPv4Address(); + return is_ip_internal(ipv4.toString()); + } catch { + return true; + } + } + return true; + } + + // Additional manual blocks for IPv6 + const extraBadRanges = [ + ipaddr.parseCIDR("64:ff9b:1::/48"), + ipaddr.parseCIDR("5f00::/8"), + ipaddr.parseCIDR("3fff::/20"), + ipaddr.parseCIDR("fec0::/10"), + ]; + + for (const [range, bits] of extraBadRanges) { + // @ts-ignore + if (parsed.match(range, bits)) return true; + } } return false; @@ -491,7 +513,7 @@ export async function is_url_safe(url: string): Promise { if (!is_proto_safe(schema)) return false; const parsed = new URL(u); - const hostname = parsed.hostname; + const hostname = parsed.hostname.replace(/^\[|\]$/g, ""); // IPv4 validation if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) { @@ -521,6 +543,6 @@ export async function is_url_safe(url: string): Promise { /// FIXME(): The debug version do not match is_url_safe, We'wll fix it later but for now keep it as is. -export async function is_url_safe_debug(url: string): Promise { try { console.log("STEP 1 input:", url); let u = normalize_unicode(url); console.log("STEP 2 unicode:", u); u = replace_backslash_with_slash_in_string(u); console.log("STEP 3 slashes:", u); u = replace_two_slashes_url_to_normal_url(u); console.log("STEP 4 normalize slashes:", u); u = remove_at_symbol_in_string(u); console.log("STEP 5 remove @:", u); const schema = normalize_schema(u); if (!is_proto_safe(schema)) return false; console.log("STEP 6 schema:", schema); if (!is_proto_safe(u)) { console.log("STEP 7 proto unsafe"); return false; } console.log("STEP 7 proto safe"); const parsed = new URL(u); const hostname = parsed.hostname; console.log("STEP 8 hostname:", hostname); if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) { try { normalize_ipv4(hostname); } catch { console.log("STEP 9 ipv4 invalid"); return false; } } if (is_ipv6(hostname)) { if (is_ip_internal(hostname)) { console.log("STEP 10 ipv6 internal"); return false; } } const isInternal = await is_hostname_resolve_to_internal_ip(hostname); console.log("STEP 11 internal?", isInternal); if (isInternal) { return false; } const redirectSafe = await is_redirect_safe(u); console.log("STEP 12 redirect safe?", redirectSafe); if (!redirectSafe) { return false; } console.log("STEP 13 final: true"); return true; } catch (e) { console.log("ERROR:", e); return false; } } +export async function is_url_safe_debug(url: string): Promise { try { console.log("STEP 1 input:", url); let u = normalize_unicode(url); console.log("STEP 2 unicode:", u); u = replace_backslash_with_slash_in_string(u); console.log("STEP 3 slashes:", u); u = replace_two_slashes_url_to_normal_url(u); console.log("STEP 4 normalize slashes:", u); u = remove_at_symbol_in_string(u); console.log("STEP 5 remove @:", u); const schema = normalize_schema(u); if (!is_proto_safe(schema)) return false; console.log("STEP 6 schema:", schema); if (!is_proto_safe(u)) { console.log("STEP 7 proto unsafe"); return false; } console.log("STEP 7 proto safe"); const parsed = new URL(u); const hostname = parsed.hostname.replace(/^\[|\]$/g, ""); console.log("STEP 8 hostname:", hostname); if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) { try { normalize_ipv4(hostname); } catch { console.log("STEP 9 ipv4 invalid"); return false; } } if (is_ipv6(hostname)) { if (is_ip_internal(hostname)) { console.log("STEP 10 ipv6 internal"); return false; } } const isInternal = await is_hostname_resolve_to_internal_ip(hostname); console.log("STEP 11 internal?", isInternal); if (isInternal) { return false; } const redirectSafe = await is_redirect_safe(u); console.log("STEP 12 redirect safe?", redirectSafe); if (!redirectSafe) { return false; } console.log("STEP 13 final: true"); return true; } catch (e) { console.log("ERROR:", e); return false; } }