fix(network): normalize SSRF IP parsing and monitor typing

This commit is contained in:
Peter Steinberger
2026-02-22 18:55:12 +01:00
parent 4ed87a6672
commit 98427453ba
5 changed files with 65 additions and 12 deletions

View File

@@ -28,6 +28,7 @@ const PRIVATE_OR_LOOPBACK_IPV6_RANGES = new Set<Ipv6Range>([
"linkLocal",
"uniqueLocal",
]);
const RFC2544_BENCHMARK_PREFIX: [ipaddr.IPv4, number] = [ipaddr.IPv4.parse("198.18.0.0"), 15];
const EMBEDDED_IPV4_SENTINEL_RULES: Array<{
matches: (parts: number[]) => boolean;
@@ -79,6 +80,32 @@ function stripIpv6Brackets(value: string): string {
return value;
}
function isNumericIpv4LiteralPart(value: string): boolean {
return /^[0-9]+$/.test(value) || /^0x[0-9a-f]+$/i.test(value);
}
function parseIpv6WithEmbeddedIpv4(raw: string): ipaddr.IPv6 | undefined {
if (!raw.includes(":") || !raw.includes(".")) {
return undefined;
}
const match = /^(.*:)([^:%]+(?:\.[^:%]+){3})(%[0-9A-Za-z]+)?$/i.exec(raw);
if (!match) {
return undefined;
}
const [, prefix, embeddedIpv4, zoneSuffix = ""] = match;
if (!ipaddr.IPv4.isValidFourPartDecimal(embeddedIpv4)) {
return undefined;
}
const octets = embeddedIpv4.split(".").map((part) => Number.parseInt(part, 10));
const high = ((octets[0] << 8) | octets[1]).toString(16);
const low = ((octets[2] << 8) | octets[3]).toString(16);
const normalizedIpv6 = `${prefix}${high}:${low}${zoneSuffix}`;
if (!ipaddr.IPv6.isValid(normalizedIpv6)) {
return undefined;
}
return ipaddr.IPv6.parse(normalizedIpv6);
}
export function isIpv4Address(address: ParsedIpAddress): address is ipaddr.IPv4 {
return address.kind() === "ipv4";
}
@@ -115,7 +142,7 @@ export function parseCanonicalIpAddress(raw: string | undefined): ParsedIpAddres
if (ipaddr.IPv6.isValid(normalized)) {
return ipaddr.IPv6.parse(normalized);
}
return undefined;
return parseIpv6WithEmbeddedIpv4(normalized);
}
export function parseLooseIpAddress(raw: string | undefined): ParsedIpAddress | undefined {
@@ -127,10 +154,10 @@ export function parseLooseIpAddress(raw: string | undefined): ParsedIpAddress |
if (!normalized) {
return undefined;
}
if (!ipaddr.isValid(normalized)) {
return undefined;
if (ipaddr.isValid(normalized)) {
return ipaddr.parse(normalized);
}
return ipaddr.parse(normalized);
return parseIpv6WithEmbeddedIpv4(normalized);
}
export function normalizeIpAddress(raw: string | undefined): string | undefined {
@@ -163,7 +190,20 @@ export function isLegacyIpv4Literal(raw: string | undefined): boolean {
if (!normalized || normalized.includes(":")) {
return false;
}
return ipaddr.IPv4.isValid(normalized) && !ipaddr.IPv4.isValidFourPartDecimal(normalized);
if (isCanonicalDottedDecimalIPv4(normalized)) {
return false;
}
const parts = normalized.split(".");
if (parts.length === 0 || parts.length > 4) {
return false;
}
if (parts.some((part) => part.length === 0)) {
return false;
}
if (!parts.every((part) => isNumericIpv4LiteralPart(part))) {
return false;
}
return true;
}
export function isLoopbackIpAddress(raw: string | undefined): boolean {
@@ -208,7 +248,9 @@ export function isCarrierGradeNatIpv4Address(raw: string | undefined): boolean {
}
export function isBlockedSpecialUseIpv4Address(address: ipaddr.IPv4): boolean {
return BLOCKED_IPV4_SPECIAL_USE_RANGES.has(address.range());
return (
BLOCKED_IPV4_SPECIAL_USE_RANGES.has(address.range()) || address.match(RFC2544_BENCHMARK_PREFIX)
);
}
function decodeIpv4FromHextets(high: number, low: number): ipaddr.IPv4 {
@@ -276,7 +318,13 @@ export function isIpInCidr(ip: string, cidr: string): boolean {
return false;
}
try {
return comparableIp.match([comparableBase, prefixLength]);
if (isIpv4Address(comparableIp) && isIpv4Address(comparableBase)) {
return comparableIp.match([comparableBase, prefixLength]);
}
if (isIpv6Address(comparableIp) && isIpv6Address(comparableBase)) {
return comparableIp.match([comparableBase, prefixLength]);
}
return false;
} catch {
return false;
}