Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -491,7 +513,7 @@ export async function is_url_safe(url: string): Promise<boolean> {
if (!is_proto_safe(schema)) return false;

const parsed = new URL(u);
const hostname = parsed.hostname;
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Redirect ipv6 normalization gap 🐞 Bug ⛨ Security

is_url_safe() now strips IPv6 literal brackets before internal/DNS checks, but is_redirect_safe()
still passes current.hostname directly into is_hostname_resolve_to_internal_ip(), so redirect-hop
safety can differ from the main URL safety logic for bracketed IPv6 literals.
Agent Prompt
### Issue description
`is_url_safe()` normalizes IPv6-literal hostnames by stripping `[` and `]` before calling `is_hostname_resolve_to_internal_ip()`, but `is_redirect_safe()` does not. This can make redirect-hop validation inconsistent with the primary URL validation for IPv6 literals.

### Issue Context
- `is_url_safe()` now does `parsed.hostname.replace(/^\[|\]$/g, "")`.
- `is_redirect_safe()` currently passes `current.hostname` directly into `is_hostname_resolve_to_internal_ip()`.
- `is_hostname_resolve_to_internal_ip()` only performs direct-IP classification when `ipaddr.isValid(host)` is true.

### Fix Focus Areas
- src/helpers.ts[451-477]
- src/helpers.ts[310-317]
- src/helpers.ts[504-532]

### Suggested fix
Prefer a single canonical normalization point:
1) Add bracket-stripping inside `is_hostname_resolve_to_internal_ip()` (e.g., `const host = hostname.trim().replace(/^\[|\]$/g, "");`) so *all* callers are protected.
2) Optionally also normalize `current.hostname` in `is_redirect_safe()` for clarity/defense-in-depth.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


// IPv4 validation
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
Expand Down Expand Up @@ -521,6 +543,6 @@ export async function is_url_safe(url: string): Promise<boolean> {


/// 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<boolean> { 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<boolean> { 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; } }


Loading