import { lookup as dnsLookupCb, type LookupAddress } from "node:dns"; import { lookup as dnsLookup } from "node:dns/promises"; import type { Dispatcher } from "undici"; import {
extractEmbeddedIpv4FromIpv6,
isBlockedSpecialUseIpv4Address,
isBlockedSpecialUseIpv6Address,
isCanonicalDottedDecimalIPv4,
type Ipv4SpecialUseBlockOptions,
isIpv4Address,
isLegacyIpv4Literal,
parseCanonicalIpAddress,
parseLooseIpAddress,
} from "../../shared/net/ip.js"; import { normalizeHostname } from "./hostname.js"; import {
createHttp1Agent,
createHttp1EnvHttpProxyAgent,
createHttp1ProxyAgent,
} from "./undici-runtime.js";
function looksLikeUnsupportedIpv4Literal(address: string): boolean { const parts = address.split("."); if (parts.length === 0 || parts.length > 4) { returnfalse;
} if (parts.some((part) => part.length === 0)) { returntrue;
} // Tighten only "ipv4-ish" literals (numbers + optional 0x prefix). Hostnames like // "example.com" must stay in hostname policy handling and not be treated as malformed IPs. return parts.every((part) => /^[0-9]+$/.test(part) || /^0x/i.test(part));
}
// Returns true for private/internal and special-use non-global addresses.
export function isPrivateIpAddress(address: string, policy?: SsrFPolicy): boolean { const normalized = normalizeHostname(address); if (!normalized) { returnfalse;
} const blockOptions = resolveIpv4SpecialUseBlockOptions(policy);
const strictIp = parseCanonicalIpAddress(normalized); if (strictIp) { if (isIpv4Address(strictIp)) { return isBlockedSpecialUseIpv4Address(strictIp, blockOptions);
} if (isBlockedSpecialUseIpv6Address(strictIp)) { returntrue;
} const embeddedIpv4 = extractEmbeddedIpv4FromIpv6(strictIp); if (embeddedIpv4) { return isBlockedSpecialUseIpv4Address(embeddedIpv4, blockOptions);
} returnfalse;
}
// Security-critical parse failures should fail closed for any malformed IPv6 literal. if (normalized.includes(":") && !parseLooseIpAddress(normalized)) { returntrue;
}
if (!isCanonicalDottedDecimalIPv4(normalized) && isLegacyIpv4Literal(normalized)) { returntrue;
} if (looksLikeUnsupportedIpv4Literal(normalized)) { returntrue;
} returnfalse;
}
export function isBlockedHostname(hostname: string): boolean { const normalized = normalizeHostname(hostname); if (!normalized) { returnfalse;
} return isBlockedHostnameNormalized(normalized);
}
function isBlockedHostnameNormalized(normalized: string): boolean { if (BLOCKED_HOSTNAMES.has(normalized)) { returntrue;
} return (
normalized.endsWith(".localhost") ||
normalized.endsWith(".local") ||
normalized.endsWith(".internal")
);
}
const BLOCKED_HOST_OR_IP_MESSAGE = "Blocked hostname or private/internal/special-use IP address"; const BLOCKED_RESOLVED_IP_MESSAGE = "Blocked: resolves to private/internal/special-use IP address";
function assertAllowedHostOrIpOrThrow(hostnameOrIp: string, policy?: SsrFPolicy): void { if (isBlockedHostnameOrIp(hostnameOrIp, policy)) { thrownew SsrFBlockedError(BLOCKED_HOST_OR_IP_MESSAGE);
}
}
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) { thrownew SsrFBlockedError(`Blocked hostname (not in allowlist): ${hostname}`);
}
if (!skipPrivateNetworkChecks) { // Fail fast for literal hosts/IPs before any DNS lookup side-effects.
assertAllowedHostOrIpOrThrow(normalized, policy);
}
function assertAllowedResolvedAddressesOrThrow(
results: readonly LookupAddress[],
policy?: SsrFPolicy,
): void { for (const entry of results) { // Reuse the exact same host/IP classifier as the pre-DNS check to avoid drift. if (isBlockedHostnameOrIp(entry.address, policy)) { thrownew SsrFBlockedError(BLOCKED_RESOLVED_IP_MESSAGE);
}
}
}
function normalizeLookupResults(results: LookupResult): readonly LookupAddress[] { if (Array.isArray(results)) { return results;
} return [results];
}
export function createPinnedLookup(params: {
hostname: string;
addresses: string[];
fallback?: typeof dnsLookupCb;
}): typeof dnsLookupCb { const normalizedHost = normalizeHostname(params.hostname); if (params.addresses.length === 0) { thrownew Error(`Pinned lookup requires at least one address for ${params.hostname}`);
} const fallback = params.fallback ?? dnsLookupCb; const fallbackLookup = fallback as unknown as (
hostname: string,
callback: LookupCallback,
) => void; const fallbackWithOptions = fallback as unknown as (
hostname: string,
options: unknown,
callback: LookupCallback,
) => void; const records = params.addresses.map((address) => ({
address,
family: address.includes(":") ? 6 : 4,
}));
let index = 0;
if (!skipPrivateNetworkChecks) { // Phase 2: re-check DNS answers so public hostnames cannot pivot to private targets.
assertAllowedResolvedAddressesOrThrow(results, params.policy);
}
// Prefer addresses returned as IPv4 by DNS family metadata before other // families so Happy Eyeballs and pinned round-robin both attempt IPv4 first. const addresses = dedupeAndPreferIpv4(results); if (addresses.length === 0) { thrownew Error(`Unable to resolve hostname: ${hostname}`);
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.