refactor: remove old net policy sources

This commit is contained in:
Peter Steinberger
2026-05-29 09:44:57 +01:00
parent f4c6c0aec4
commit 25b3c8ef71
8 changed files with 0 additions and 826 deletions

View File

@@ -1 +0,0 @@
export const blockedIpv6MulticastLiterals = ["ff02::1", "ff05::1:3", "[ff02::1]"] as const;

View File

@@ -1,187 +0,0 @@
import { describe, expect, it } from "vitest";
import { blockedIpv6MulticastLiterals } from "./ip-test-fixtures.js";
import {
extractEmbeddedIpv4FromIpv6,
isBlockedSpecialUseIpv4Address,
isBlockedSpecialUseIpv6Address,
isCanonicalDottedDecimalIPv4,
isCarrierGradeNatIpv4Address,
isCloudMetadataIpAddress,
isIpInCidr,
isIpv4Address,
isIpv6Address,
isLegacyIpv4Literal,
isLinkLocalIpAddress,
isLoopbackIpAddress,
isPrivateOrLoopbackIpAddress,
isRfc1918Ipv4Address,
normalizeIpAddress,
parseCanonicalIpAddress,
parseLooseIpAddress,
} from "./ip.js";
describe("shared ip helpers", () => {
it("distinguishes canonical dotted IPv4 from legacy forms", () => {
expect(isCanonicalDottedDecimalIPv4("127.0.0.1")).toBe(true);
expect(isCanonicalDottedDecimalIPv4("0177.0.0.1")).toBe(false);
expect(isLegacyIpv4Literal("0177.0.0.1")).toBe(true);
expect(isLegacyIpv4Literal("127.1")).toBe(true);
expect(isLegacyIpv4Literal("example.com")).toBe(false);
});
it("matches both IPv4 and IPv6 CIDRs", () => {
expect(isIpInCidr("10.42.0.59", "10.42.0.0/24")).toBe(true);
expect(isIpInCidr("10.43.0.59", "10.42.0.0/24")).toBe(false);
expect(isIpInCidr("2001:db8::1234", "2001:db8::/32")).toBe(true);
expect(isIpInCidr("2001:db9::1234", "2001:db8::/32")).toBe(false);
expect(isIpInCidr("::ffff:127.0.0.1", "127.0.0.1")).toBe(true);
expect(isIpInCidr("127.0.0.1", "::ffff:127.0.0.2")).toBe(false);
});
it("extracts embedded IPv4 for transition prefixes", () => {
const cases = [
["::ffff:127.0.0.1", "127.0.0.1"],
["::127.0.0.1", "127.0.0.1"],
["64:ff9b::8.8.8.8", "8.8.8.8"],
["64:ff9b:1::10.0.0.1", "10.0.0.1"],
["2002:0808:0808::", "8.8.8.8"],
["2001::f7f7:f7f7", "8.8.8.8"],
["2001:4860:1::5efe:7f00:1", "127.0.0.1"],
] as const;
for (const [ipv6Literal, expectedIpv4] of cases) {
const parsed = parseCanonicalIpAddress(ipv6Literal);
expect(parsed?.kind(), ipv6Literal).toBe("ipv6");
if (!parsed || !isIpv6Address(parsed)) {
continue;
}
expect(extractEmbeddedIpv4FromIpv6(parsed)?.toString(), ipv6Literal).toBe(expectedIpv4);
}
});
it("treats blocked IPv6 classes as private/internal", () => {
expect(isPrivateOrLoopbackIpAddress("fec0::1")).toBe(true);
expect(isPrivateOrLoopbackIpAddress("2001:db8::1")).toBe(true);
expect(isPrivateOrLoopbackIpAddress("2001:2::1")).toBe(true);
expect(isPrivateOrLoopbackIpAddress("100::1")).toBe(true);
expect(isPrivateOrLoopbackIpAddress("2001:20::1")).toBe(true);
for (const literal of blockedIpv6MulticastLiterals) {
expect(isPrivateOrLoopbackIpAddress(literal)).toBe(true);
}
expect(isPrivateOrLoopbackIpAddress("2001:4860:4860::8888")).toBe(false);
});
it("normalizes canonical IP strings and loopback detection", () => {
expect(normalizeIpAddress("[::FFFF:127.0.0.1]")).toBe("127.0.0.1");
expect(normalizeIpAddress(" [2001:DB8::1] ")).toBe("2001:db8::1");
expect(isLoopbackIpAddress("::ffff:127.0.0.1")).toBe(true);
expect(isLoopbackIpAddress("198.18.0.1")).toBe(false);
});
it("detects link-local addresses without treating normal private ranges as link-local", () => {
expect(isLinkLocalIpAddress("169.254.169.254")).toBe(true);
expect(isLinkLocalIpAddress("::ffff:169.254.169.254")).toBe(true);
expect(isLinkLocalIpAddress("2852039166")).toBe(true);
expect(isLinkLocalIpAddress("0xa9fea9fe")).toBe(true);
expect(isLinkLocalIpAddress("0xa9.0xfe.0xa9.0xfe")).toBe(true);
expect(isLinkLocalIpAddress("64:ff9b::169.254.169.254")).toBe(true);
expect(isLinkLocalIpAddress("64:ff9b:1::a9fe:a9fe")).toBe(true);
expect(isLinkLocalIpAddress("2002:a9fe:a9fe::")).toBe(true);
expect(isLinkLocalIpAddress("fe80::1%lo0")).toBe(true);
expect(isLinkLocalIpAddress("[fe80::1]")).toBe(true);
expect(isLinkLocalIpAddress("10.0.0.5")).toBe(false);
expect(isLinkLocalIpAddress("127.0.0.1")).toBe(false);
expect(isLinkLocalIpAddress("fd00::1")).toBe(false);
});
it("detects known non-link-local cloud metadata IPs", () => {
expect(isCloudMetadataIpAddress("100.100.100.200")).toBe(true);
expect(isCloudMetadataIpAddress("::ffff:100.100.100.200")).toBe(true);
expect(isCloudMetadataIpAddress("64:ff9b::100.100.100.200")).toBe(true);
expect(isCloudMetadataIpAddress("64:ff9b:1::6464:64c8")).toBe(true);
expect(isCloudMetadataIpAddress("2002:6464:64c8::")).toBe(true);
expect(isCloudMetadataIpAddress("1684301000")).toBe(true);
expect(isCloudMetadataIpAddress("fd00:ec2::254")).toBe(true);
expect(isCloudMetadataIpAddress("[fd00:ec2::254]")).toBe(true);
expect(isCloudMetadataIpAddress("100.100.100.201")).toBe(false);
expect(isCloudMetadataIpAddress("169.254.169.254")).toBe(false);
expect(isCloudMetadataIpAddress("fd00::1")).toBe(false);
});
it("parses loose legacy IPv4 literals that canonical parsing rejects", () => {
expect(parseCanonicalIpAddress("0177.0.0.1")).toBeUndefined();
expect(parseLooseIpAddress("0177.0.0.1")?.toString()).toBe("127.0.0.1");
expect(parseLooseIpAddress("[::1]")?.toString()).toBe("::1");
});
it("classifies RFC1918 and carrier-grade-nat IPv4 ranges", () => {
expect(isRfc1918Ipv4Address("10.42.0.59")).toBe(true);
expect(isRfc1918Ipv4Address("100.64.0.1")).toBe(false);
expect(isCarrierGradeNatIpv4Address("100.64.0.1")).toBe(true);
expect(isCarrierGradeNatIpv4Address("10.42.0.59")).toBe(false);
});
it("blocks special-use IPv4 ranges while allowing optional RFC2544 benchmark addresses", () => {
const loopback = parseCanonicalIpAddress("127.0.0.1");
const benchmark = parseCanonicalIpAddress("198.18.0.1");
expect(loopback?.kind()).toBe("ipv4");
expect(benchmark?.kind()).toBe("ipv4");
if (!loopback || !isIpv4Address(loopback) || !benchmark || !isIpv4Address(benchmark)) {
throw new Error("expected ipv4 fixtures");
}
expect(isBlockedSpecialUseIpv4Address(loopback)).toBe(true);
expect(isBlockedSpecialUseIpv4Address(benchmark)).toBe(true);
expect(isBlockedSpecialUseIpv4Address(benchmark, { allowRfc2544BenchmarkRange: true })).toBe(
false,
);
});
it("blocks IPv6 unique-local addresses by default and exempts them on opt-in (#74351)", () => {
// fc00::/7 is the IPv6 ULA range. Sing-box / Clash / Surge fake-ip
// proxies resolve foreign domains here, alongside the IPv4 198.18.0.0/15
// benchmark range. Operators using those proxies need both ranges
// exempted to keep web_fetch working.
const ula = parseCanonicalIpAddress("fc00::1");
expect(ula?.kind()).toBe("ipv6");
if (!ula || !isIpv6Address(ula)) {
throw new Error("expected ipv6 fixture");
}
// Default policy (no options) must continue to block the ULA range.
expect(isBlockedSpecialUseIpv6Address(ula)).toBe(true);
expect(isBlockedSpecialUseIpv6Address(ula, {})).toBe(true);
expect(isBlockedSpecialUseIpv6Address(ula, { allowUniqueLocalRange: false })).toBe(true);
// Opt-in flag — the only path the SSRF policy uses to thread fake-ip
// proxy intent through to the address classifier.
expect(isBlockedSpecialUseIpv6Address(ula, { allowUniqueLocalRange: true })).toBe(false);
});
it("opt-in unique-local exemption does NOT bleed into other special-use IPv6 ranges (#74351)", () => {
// The exemption must be scoped: loopback (::1), unspecified (::), and
// multicast (ff00::/8) all stay blocked even when `allowUniqueLocalRange`
// is set, otherwise the flag silently widens the SSRF escape hatch
// beyond what operators opted into.
const loopback = parseCanonicalIpAddress("::1");
const multicast = parseCanonicalIpAddress("ff02::1");
const siteLocal = parseCanonicalIpAddress("fec0::1"); // deprecated fec0::/10
if (
!loopback ||
!isIpv6Address(loopback) ||
!multicast ||
!isIpv6Address(multicast) ||
!siteLocal ||
!isIpv6Address(siteLocal)
) {
throw new Error("expected ipv6 fixtures");
}
for (const options of [{}, { allowUniqueLocalRange: true }] as const) {
expect(isBlockedSpecialUseIpv6Address(loopback, options)).toBe(true);
expect(isBlockedSpecialUseIpv6Address(multicast, options)).toBe(true);
expect(isBlockedSpecialUseIpv6Address(siteLocal, options)).toBe(true);
}
});
});

View File

@@ -1,412 +0,0 @@
import ipaddr from "ipaddr.js";
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.js";
export type ParsedIpAddress = ipaddr.IPv4 | ipaddr.IPv6;
type Ipv4Range = ReturnType<ipaddr.IPv4["range"]>;
type Ipv6Range = ReturnType<ipaddr.IPv6["range"]>;
type BlockedIpv6Range = Ipv6Range | "discard";
const BLOCKED_IPV4_SPECIAL_USE_RANGES = new Set<Ipv4Range>([
"unspecified",
"broadcast",
"multicast",
"linkLocal",
"loopback",
"carrierGradeNat",
"private",
"reserved",
]);
const PRIVATE_OR_LOOPBACK_IPV4_RANGES = new Set<Ipv4Range>([
"loopback",
"private",
"linkLocal",
"carrierGradeNat",
]);
const BLOCKED_IPV6_SPECIAL_USE_RANGES = new Set<BlockedIpv6Range>([
"unspecified",
"loopback",
"linkLocal",
"uniqueLocal",
"multicast",
"reserved",
"benchmarking",
"discard",
"orchid2",
]);
const RFC2544_BENCHMARK_PREFIX: [ipaddr.IPv4, number] = [ipaddr.IPv4.parse("198.18.0.0"), 15];
const CLOUD_METADATA_IP_ADDRESSES = new Set(["100.100.100.200", "fd00:ec2::254"]);
export type Ipv4SpecialUseBlockOptions = {
allowRfc2544BenchmarkRange?: boolean;
};
/**
* Per-call exemptions for `isBlockedSpecialUseIpv6Address`. Mirror of
* {@link Ipv4SpecialUseBlockOptions} for the IPv6 side. Currently only
* `allowUniqueLocalRange` is exposed (#74351); other reserved IPv6 ranges stay
* unconditionally blocked because they have no documented fake-ip / proxy
* use case.
*/
export type Ipv6SpecialUseBlockOptions = {
/**
* When true, exempt addresses in `fc00::/7` (the IPv6 Unique Local Address
* block, RFC 4193) from the SSRF private-IP block. Sing-box / Clash / Surge
* fake-ip implementations resolve foreign domains to ULA addresses
* alongside RFC 2544 benchmark IPv4 addresses, and operators using those
* proxy stacks need both ranges exempted to keep `web_fetch` working.
*/
allowUniqueLocalRange?: boolean;
};
const EMBEDDED_IPV4_SENTINEL_RULES: Array<{
matches: (parts: number[]) => boolean;
toHextets: (parts: number[]) => [high: number, low: number];
}> = [
{
// IPv4-compatible form ::w.x.y.z (deprecated, but still seen in parser edge-cases).
matches: (parts) =>
parts[0] === 0 &&
parts[1] === 0 &&
parts[2] === 0 &&
parts[3] === 0 &&
parts[4] === 0 &&
parts[5] === 0,
toHextets: (parts) => [parts[6], parts[7]],
},
{
// NAT64 local-use prefix: 64:ff9b:1::/48.
matches: (parts) =>
parts[0] === 0x0064 &&
parts[1] === 0xff9b &&
parts[2] === 0x0001 &&
parts[3] === 0 &&
parts[4] === 0 &&
parts[5] === 0,
toHextets: (parts) => [parts[6], parts[7]],
},
{
// 6to4 prefix: 2002::/16 (IPv4 lives in hextets 1..2).
matches: (parts) => parts[0] === 0x2002,
toHextets: (parts) => [parts[1], parts[2]],
},
{
// Teredo prefix: 2001:0000::/32 (client IPv4 XOR 0xffff in hextets 6..7).
matches: (parts) => parts[0] === 0x2001 && parts[1] === 0x0000,
toHextets: (parts) => [parts[6] ^ 0xffff, parts[7] ^ 0xffff],
},
{
// ISATAP IID marker: ....:0000:5efe:w.x.y.z with u/g bits allowed in hextet 4.
matches: (parts) => (parts[4] & 0xfcff) === 0 && parts[5] === 0x5efe,
toHextets: (parts) => [parts[6], parts[7]],
},
];
function stripIpv6Brackets(value: string): string {
if (value.startsWith("[") && value.endsWith("]")) {
return value.slice(1, -1);
}
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";
}
export function isIpv6Address(address: ParsedIpAddress): address is ipaddr.IPv6 {
return address.kind() === "ipv6";
}
function normalizeIpv4MappedAddress(address: ParsedIpAddress): ParsedIpAddress {
if (!isIpv6Address(address)) {
return address;
}
if (!address.isIPv4MappedAddress()) {
return address;
}
return address.toIPv4Address();
}
function normalizeIpParseInput(raw: string | undefined): string | undefined {
const trimmed = normalizeOptionalString(raw);
if (!trimmed) {
return undefined;
}
return stripIpv6Brackets(trimmed);
}
export function parseCanonicalIpAddress(raw: string | undefined): ParsedIpAddress | undefined {
const normalized = normalizeIpParseInput(raw);
if (!normalized) {
return undefined;
}
if (ipaddr.IPv4.isValid(normalized)) {
if (!ipaddr.IPv4.isValidFourPartDecimal(normalized)) {
return undefined;
}
return ipaddr.IPv4.parse(normalized);
}
if (ipaddr.IPv6.isValid(normalized)) {
return ipaddr.IPv6.parse(normalized);
}
return parseIpv6WithEmbeddedIpv4(normalized);
}
export function parseLooseIpAddress(raw: string | undefined): ParsedIpAddress | undefined {
const normalized = normalizeIpParseInput(raw);
if (!normalized) {
return undefined;
}
if (ipaddr.isValid(normalized)) {
return ipaddr.parse(normalized);
}
return parseIpv6WithEmbeddedIpv4(normalized);
}
export function normalizeIpAddress(raw: string | undefined): string | undefined {
const parsed = parseCanonicalIpAddress(raw);
if (!parsed) {
return undefined;
}
const normalized = normalizeIpv4MappedAddress(parsed);
return normalizeLowercaseStringOrEmpty(normalized.toString());
}
export function isCanonicalDottedDecimalIPv4(raw: string | undefined): boolean {
const trimmed = normalizeOptionalString(raw);
if (!trimmed) {
return false;
}
const normalized = stripIpv6Brackets(trimmed);
if (!normalized) {
return false;
}
return ipaddr.IPv4.isValidFourPartDecimal(normalized);
}
export function isLegacyIpv4Literal(raw: string | undefined): boolean {
const trimmed = normalizeOptionalString(raw);
if (!trimmed) {
return false;
}
const normalized = stripIpv6Brackets(trimmed);
if (!normalized || normalized.includes(":")) {
return false;
}
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 {
const parsed = parseCanonicalIpAddress(raw);
if (!parsed) {
return false;
}
const normalized = normalizeIpv4MappedAddress(parsed);
return normalized.range() === "loopback";
}
export function isLinkLocalIpAddress(raw: string | undefined): boolean {
const parsed = parseLooseIpAddress(raw);
if (!parsed) {
return false;
}
const normalized = normalizeIpv4MappedAddress(parsed);
if (isIpv4Address(normalized)) {
return normalized.range() === "linkLocal";
}
const embeddedIpv4 = extractEmbeddedIpv4FromIpv6(normalized);
if (embeddedIpv4?.range() === "linkLocal") {
return true;
}
return normalized.range() === "linkLocal";
}
export function isCloudMetadataIpAddress(raw: string | undefined): boolean {
const parsed = parseLooseIpAddress(raw);
if (!parsed) {
return false;
}
const normalized = normalizeIpv4MappedAddress(parsed);
if (isIpv6Address(normalized)) {
const embeddedIpv4 = extractEmbeddedIpv4FromIpv6(normalized);
if (embeddedIpv4 && CLOUD_METADATA_IP_ADDRESSES.has(embeddedIpv4.toString())) {
return true;
}
}
return CLOUD_METADATA_IP_ADDRESSES.has(normalized.toString());
}
export function isPrivateOrLoopbackIpAddress(raw: string | undefined): boolean {
const parsed = parseCanonicalIpAddress(raw);
if (!parsed) {
return false;
}
const normalized = normalizeIpv4MappedAddress(parsed);
if (isIpv4Address(normalized)) {
return PRIVATE_OR_LOOPBACK_IPV4_RANGES.has(normalized.range());
}
return isBlockedSpecialUseIpv6Address(normalized);
}
export function isBlockedSpecialUseIpv6Address(
address: ipaddr.IPv6,
options: Ipv6SpecialUseBlockOptions = {},
): boolean {
// ipaddr.js returns "discard" at runtime for 100::/64, but its published
// TypeScript IPv6Range union omits that literal.
const range = address.range() as BlockedIpv6Range;
if (range === "uniqueLocal" && options.allowUniqueLocalRange === true) {
// Operators running fake-ip proxy stacks (sing-box, Clash, Surge) opt in
// to fc00::/7 reaching the network — same intent as
// `allowRfc2544BenchmarkRange` for the IPv4 side (#74351).
return false;
}
if (BLOCKED_IPV6_SPECIAL_USE_RANGES.has(range)) {
return true;
}
// ipaddr.js does not classify deprecated site-local fec0::/10 as private.
return (address.parts[0] & 0xffc0) === 0xfec0;
}
export function isRfc1918Ipv4Address(raw: string | undefined): boolean {
const parsed = parseCanonicalIpAddress(raw);
if (!parsed || !isIpv4Address(parsed)) {
return false;
}
return parsed.range() === "private";
}
export function isCarrierGradeNatIpv4Address(raw: string | undefined): boolean {
const parsed = parseCanonicalIpAddress(raw);
if (!parsed || !isIpv4Address(parsed)) {
return false;
}
return parsed.range() === "carrierGradeNat";
}
export function isBlockedSpecialUseIpv4Address(
address: ipaddr.IPv4,
options: Ipv4SpecialUseBlockOptions = {},
): boolean {
const inRfc2544BenchmarkRange = address.match(RFC2544_BENCHMARK_PREFIX);
if (inRfc2544BenchmarkRange && options.allowRfc2544BenchmarkRange === true) {
return false;
}
return BLOCKED_IPV4_SPECIAL_USE_RANGES.has(address.range()) || inRfc2544BenchmarkRange;
}
function decodeIpv4FromHextets(high: number, low: number): ipaddr.IPv4 {
const octets: [number, number, number, number] = [
(high >>> 8) & 0xff,
high & 0xff,
(low >>> 8) & 0xff,
low & 0xff,
];
return ipaddr.IPv4.parse(octets.join("."));
}
export function extractEmbeddedIpv4FromIpv6(address: ipaddr.IPv6): ipaddr.IPv4 | undefined {
if (address.isIPv4MappedAddress()) {
return address.toIPv4Address();
}
if (address.range() === "rfc6145") {
return decodeIpv4FromHextets(address.parts[6], address.parts[7]);
}
if (address.range() === "rfc6052") {
return decodeIpv4FromHextets(address.parts[6], address.parts[7]);
}
for (const rule of EMBEDDED_IPV4_SENTINEL_RULES) {
if (!rule.matches(address.parts)) {
continue;
}
const [high, low] = rule.toHextets(address.parts);
return decodeIpv4FromHextets(high, low);
}
return undefined;
}
export function isIpInCidr(ip: string, cidr: string): boolean {
const normalizedIp = parseCanonicalIpAddress(ip);
if (!normalizedIp) {
return false;
}
const candidate = cidr.trim();
if (!candidate) {
return false;
}
const comparableIp = normalizeIpv4MappedAddress(normalizedIp);
if (!candidate.includes("/")) {
const exact = parseCanonicalIpAddress(candidate);
if (!exact) {
return false;
}
const comparableExact = normalizeIpv4MappedAddress(exact);
return (
comparableIp.kind() === comparableExact.kind() &&
comparableIp.toString() === comparableExact.toString()
);
}
let parsedCidr: [ParsedIpAddress, number];
try {
parsedCidr = ipaddr.parseCIDR(candidate);
} catch {
return false;
}
const [baseAddress, prefixLength] = parsedCidr;
const comparableBase = normalizeIpv4MappedAddress(baseAddress);
if (comparableIp.kind() !== comparableBase.kind()) {
return false;
}
try {
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;
}
}

View File

@@ -1,35 +0,0 @@
import { describe, expect, it } from "vitest";
import { validateDottedDecimalIPv4Input, validateIPv4AddressInput } from "./ipv4.js";
describe("shared/net/ipv4", () => {
it("requires a value for custom bind mode", () => {
expect(validateDottedDecimalIPv4Input(undefined)).toBe(
"IP address is required for custom bind mode",
);
expect(validateDottedDecimalIPv4Input("")).toBe("IP address is required for custom bind mode");
expect(validateDottedDecimalIPv4Input(" ")).toBe(
"Invalid IPv4 address (e.g., 192.168.1.100)",
);
});
it("accepts canonical dotted-decimal ipv4 only", () => {
expect(validateDottedDecimalIPv4Input("0.0.0.0")).toBeUndefined();
expect(validateDottedDecimalIPv4Input("192.168.1.100")).toBeUndefined();
expect(validateDottedDecimalIPv4Input(" 192.168.1.100 ")).toBeUndefined();
expect(validateDottedDecimalIPv4Input("0177.0.0.1")).toBe(
"Invalid IPv4 address (e.g., 192.168.1.100)",
);
expect(validateDottedDecimalIPv4Input("[192.168.1.100]")).toBeUndefined();
expect(validateDottedDecimalIPv4Input("127.1")).toBe(
"Invalid IPv4 address (e.g., 192.168.1.100)",
);
expect(validateDottedDecimalIPv4Input("example.com")).toBe(
"Invalid IPv4 address (e.g., 192.168.1.100)",
);
});
it("keeps the backward-compatible alias wired to the same validation", () => {
expect(validateIPv4AddressInput("192.168.1.100")).toBeUndefined();
expect(validateIPv4AddressInput("bad-ip")).toBe("Invalid IPv4 address (e.g., 192.168.1.100)");
});
});

View File

@@ -1,16 +0,0 @@
import { isCanonicalDottedDecimalIPv4 } from "./ip.js";
export function validateDottedDecimalIPv4Input(value: string | undefined): string | undefined {
if (!value) {
return "IP address is required for custom bind mode";
}
if (isCanonicalDottedDecimalIPv4(value)) {
return undefined;
}
return "Invalid IPv4 address (e.g., 192.168.1.100)";
}
/** @deprecated Use validateDottedDecimalIPv4Input. */
export function validateIPv4AddressInput(value: string | undefined): string | undefined {
return validateDottedDecimalIPv4Input(value);
}

View File

@@ -1,85 +0,0 @@
import { describe, expect, it } from "vitest";
import {
isSensitiveUrlQueryParamName,
isSensitiveUrlConfigPath,
SENSITIVE_URL_HINT_TAG,
hasSensitiveUrlHintTag,
redactSensitiveUrl,
redactSensitiveUrlLikeString,
} from "./redact-sensitive-url.js";
describe("redactSensitiveUrl", () => {
it("redacts userinfo and sensitive query params from valid URLs", () => {
expect(redactSensitiveUrl("https://user:pass@example.com/mcp?token=secret&safe=value")).toBe(
"https://***:***@example.com/mcp?token=***&safe=value",
);
});
it("treats query param names case-insensitively", () => {
expect(redactSensitiveUrl("https://example.com/mcp?Access_Token=secret")).toBe(
"https://example.com/mcp?Access_Token=***",
);
});
it("keeps non-sensitive URLs unchanged", () => {
expect(redactSensitiveUrl("https://example.com/mcp?safe=value")).toBe(
"https://example.com/mcp?safe=value",
);
});
});
describe("redactSensitiveUrlLikeString", () => {
it("redacts invalid URL-like strings", () => {
expect(redactSensitiveUrlLikeString("//user:pass@example.com/mcp?client_secret=secret")).toBe(
"//***:***@example.com/mcp?client_secret=***",
);
});
it("redacts every URL-like userinfo occurrence in arbitrary text", () => {
expect(
redactSensitiveUrlLikeString(
"fatal https://a:b@github.com/one.git and https://c:d@github.com/two.git",
),
).toBe("fatal https://***:***@github.com/one.git and https://***:***@github.com/two.git");
});
it("redacts protocol URLs that are too malformed to parse", () => {
expect(
redactSensitiveUrlLikeString(
"wss://fallback-user:fallback-pass@[bad-host/socket?token=fallback-secret&keep=visible)",
),
).toBe("wss://***:***@[bad-host/socket?token=***&keep=visible)");
});
});
describe("isSensitiveUrlQueryParamName", () => {
it("matches the auth-oriented query params used by MCP SSE config redaction", () => {
expect(isSensitiveUrlQueryParamName("token")).toBe(true);
expect(isSensitiveUrlQueryParamName("refresh_token")).toBe(true);
expect(isSensitiveUrlQueryParamName("access-token")).toBe(true);
expect(isSensitiveUrlQueryParamName("hook-token")).toBe(true);
expect(isSensitiveUrlQueryParamName("passwd")).toBe(true);
expect(isSensitiveUrlQueryParamName("signature")).toBe(true);
expect(isSensitiveUrlQueryParamName("safe")).toBe(false);
});
});
describe("sensitive URL config metadata", () => {
it("recognizes config paths that may embed URL secrets", () => {
expect(isSensitiveUrlConfigPath("models.providers.*.baseUrl")).toBe(true);
expect(isSensitiveUrlConfigPath("mcp.servers.remote.url")).toBe(true);
expect(isSensitiveUrlConfigPath("gateway.remote.url")).toBe(false);
});
it("recognizes cdpUrl config paths as sensitive (browser CDP URLs can embed credentials)", () => {
expect(isSensitiveUrlConfigPath("browser.cdpUrl")).toBe(true);
expect(isSensitiveUrlConfigPath("browser.profiles.remote.cdpUrl")).toBe(true);
expect(isSensitiveUrlConfigPath("browser.profiles.staging.cdpUrl")).toBe(true);
});
it("uses an explicit url-secret hint tag", () => {
expect(SENSITIVE_URL_HINT_TAG).toBe("url-secret");
expect(hasSensitiveUrlHintTag({ tags: [SENSITIVE_URL_HINT_TAG] })).toBe(true);
expect(hasSensitiveUrlHintTag({ tags: ["security"] })).toBe(false);
});
});

View File

@@ -1,77 +0,0 @@
import type { ConfigUiHint } from "../config-ui-hints-types.js";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.js";
export const SENSITIVE_URL_HINT_TAG = "url-secret";
const SENSITIVE_URL_QUERY_PARAM_NAMES = new Set([
"token",
"key",
"api_key",
"apikey",
"secret",
"access_token",
"auth_token",
"password",
"pass",
"passwd",
"auth",
"client_secret",
"hook_token",
"refresh_token",
"signature",
]);
export function isSensitiveUrlQueryParamName(name: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(name).replaceAll("-", "_");
return SENSITIVE_URL_QUERY_PARAM_NAMES.has(normalized);
}
export function isSensitiveUrlConfigPath(path: string): boolean {
if (path.endsWith(".baseUrl") || path.endsWith(".httpUrl")) {
return true;
}
if (path.endsWith(".cdpUrl")) {
return true;
}
if (path.endsWith(".request.proxy.url")) {
return true;
}
return /^mcp\.servers\.(?:\*|[^.]+)\.url$/.test(path);
}
export function hasSensitiveUrlHintTag(hint: Pick<ConfigUiHint, "tags"> | undefined): boolean {
return hint?.tags?.includes(SENSITIVE_URL_HINT_TAG) === true;
}
export function redactSensitiveUrl(value: string): string {
try {
const parsed = new URL(value);
let mutated = false;
if (parsed.username || parsed.password) {
parsed.username = parsed.username ? "***" : "";
parsed.password = parsed.password ? "***" : "";
mutated = true;
}
for (const key of Array.from(parsed.searchParams.keys())) {
if (isSensitiveUrlQueryParamName(key)) {
parsed.searchParams.set(key, "***");
mutated = true;
}
}
return mutated ? parsed.toString() : value;
} catch {
return value;
}
}
export function redactSensitiveUrlLikeString(value: string): string {
const redactedUrl = redactSensitiveUrl(value);
if (redactedUrl !== value) {
return redactedUrl;
}
return value
.replace(/\/\/([^@/?#\s]+)@/g, "//***:***@")
.replace(/([?&])([^=&]+)=([^&]*)/g, (match, prefix: string, key: string) =>
isSensitiveUrlQueryParamName(key) ? `${prefix}${key}=***` : match,
);
}

View File

@@ -1,13 +0,0 @@
export function stripUrlUserInfo(value: string): string {
try {
const parsed = new URL(value);
if (!parsed.username && !parsed.password) {
return value;
}
parsed.username = "";
parsed.password = "";
return parsed.toString();
} catch {
return value;
}
}