mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
isBlockedIpv6() did not normalize ::ffff:<ipv4>, allowing the SSRF filter to be bypassed. Extract and validate via isBlockedIpv4(). Co-authored-by: Cursor <cursoragent@cursor.com>
57 lines
2.0 KiB
JavaScript
57 lines
2.0 KiB
JavaScript
// SSRF guard: block internal/private/metadata targets for server-side fetch.
|
|
|
|
const BLOCKED_HOSTNAMES = new Set(["localhost", "ip6-localhost", "ip6-loopback"]);
|
|
const BLOCKED_SUFFIXES = [".internal", ".local", ".localhost"];
|
|
|
|
// Parse dotted IPv4 to 32-bit integer, or null if not a valid IPv4 literal.
|
|
function ipv4ToInt(host) {
|
|
const parts = host.split(".");
|
|
if (parts.length !== 4) return null;
|
|
let value = 0;
|
|
for (const part of parts) {
|
|
if (!/^\d{1,3}$/.test(part)) return null;
|
|
const octet = Number(part);
|
|
if (octet > 255) return null;
|
|
value = value * 256 + octet;
|
|
}
|
|
return value >>> 0;
|
|
}
|
|
|
|
// Private/reserved IPv4 ranges as [startInt, maskBits].
|
|
const BLOCKED_V4_RANGES = [
|
|
[ipv4ToInt("0.0.0.0"), 8],
|
|
[ipv4ToInt("10.0.0.0"), 8],
|
|
[ipv4ToInt("127.0.0.0"), 8],
|
|
[ipv4ToInt("169.254.0.0"), 16],
|
|
[ipv4ToInt("172.16.0.0"), 12],
|
|
[ipv4ToInt("192.168.0.0"), 16],
|
|
];
|
|
|
|
function isBlockedIpv4(host) {
|
|
const ip = ipv4ToInt(host);
|
|
if (ip === null) return false;
|
|
return BLOCKED_V4_RANGES.some(([base, bits]) => {
|
|
const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
|
|
return (ip & mask) === (base & mask);
|
|
});
|
|
}
|
|
|
|
function isBlockedIpv6(host) {
|
|
const h = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
const v4Mapped = h.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
if (v4Mapped) return isBlockedIpv4(v4Mapped[1]);
|
|
if (h === "::1" || h === "::") return true;
|
|
return h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd");
|
|
}
|
|
|
|
// Throw if URL targets a non-public host. Caller should map to 400.
|
|
export function assertPublicUrl(rawUrl) {
|
|
const parsed = new URL(rawUrl);
|
|
const host = parsed.hostname.toLowerCase();
|
|
|
|
if (BLOCKED_HOSTNAMES.has(host)) throw new Error("Blocked URL: internal host");
|
|
if (BLOCKED_SUFFIXES.some((s) => host.endsWith(s))) throw new Error("Blocked URL: internal host");
|
|
if (isBlockedIpv4(host)) throw new Error("Blocked URL: private IP");
|
|
if (host.includes(":") && isBlockedIpv6(host)) throw new Error("Blocked URL: private IP");
|
|
}
|