File: //lib/node_modules/openclaw/dist/ssrf-B7akL5oa.js
import ipaddr from "ipaddr.js";
import { lookup } from "node:dns";
import { lookup as lookup$1 } from "node:dns/promises";
import { Agent } from "undici";
//#region src/shared/net/ip.ts
const BLOCKED_IPV4_SPECIAL_USE_RANGES = new Set([
"unspecified",
"broadcast",
"multicast",
"linkLocal",
"loopback",
"carrierGradeNat",
"private",
"reserved"
]);
const PRIVATE_OR_LOOPBACK_IPV4_RANGES = new Set([
"loopback",
"private",
"linkLocal",
"carrierGradeNat"
]);
const PRIVATE_OR_LOOPBACK_IPV6_RANGES = new Set([
"unspecified",
"loopback",
"linkLocal",
"uniqueLocal"
]);
const RFC2544_BENCHMARK_PREFIX = [ipaddr.IPv4.parse("198.18.0.0"), 15];
const EMBEDDED_IPV4_SENTINEL_RULES = [
{
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]]
},
{
matches: (parts) => parts[0] === 100 && parts[1] === 65435 && parts[2] === 1 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0,
toHextets: (parts) => [parts[6], parts[7]]
},
{
matches: (parts) => parts[0] === 8194,
toHextets: (parts) => [parts[1], parts[2]]
},
{
matches: (parts) => parts[0] === 8193 && parts[1] === 0,
toHextets: (parts) => [parts[6] ^ 65535, parts[7] ^ 65535]
},
{
matches: (parts) => (parts[4] & 64767) === 0 && parts[5] === 24318,
toHextets: (parts) => [parts[6], parts[7]]
}
];
function stripIpv6Brackets(value) {
if (value.startsWith("[") && value.endsWith("]")) return value.slice(1, -1);
return value;
}
function isNumericIpv4LiteralPart(value) {
return /^[0-9]+$/.test(value) || /^0x[0-9a-f]+$/i.test(value);
}
function parseIpv6WithEmbeddedIpv4(raw) {
if (!raw.includes(":") || !raw.includes(".")) return;
const match = /^(.*:)([^:%]+(?:\.[^:%]+){3})(%[0-9A-Za-z]+)?$/i.exec(raw);
if (!match) return;
const [, prefix, embeddedIpv4, zoneSuffix = ""] = match;
if (!ipaddr.IPv4.isValidFourPartDecimal(embeddedIpv4)) return;
const octets = embeddedIpv4.split(".").map((part) => Number.parseInt(part, 10));
const normalizedIpv6 = `${prefix}${(octets[0] << 8 | octets[1]).toString(16)}:${(octets[2] << 8 | octets[3]).toString(16)}${zoneSuffix}`;
if (!ipaddr.IPv6.isValid(normalizedIpv6)) return;
return ipaddr.IPv6.parse(normalizedIpv6);
}
function isIpv4Address(address) {
return address.kind() === "ipv4";
}
function isIpv6Address(address) {
return address.kind() === "ipv6";
}
function normalizeIpv4MappedAddress(address) {
if (!isIpv6Address(address)) return address;
if (!address.isIPv4MappedAddress()) return address;
return address.toIPv4Address();
}
function parseCanonicalIpAddress(raw) {
const trimmed = raw?.trim();
if (!trimmed) return;
const normalized = stripIpv6Brackets(trimmed);
if (!normalized) return;
if (ipaddr.IPv4.isValid(normalized)) {
if (!ipaddr.IPv4.isValidFourPartDecimal(normalized)) return;
return ipaddr.IPv4.parse(normalized);
}
if (ipaddr.IPv6.isValid(normalized)) return ipaddr.IPv6.parse(normalized);
return parseIpv6WithEmbeddedIpv4(normalized);
}
function parseLooseIpAddress(raw) {
const trimmed = raw?.trim();
if (!trimmed) return;
const normalized = stripIpv6Brackets(trimmed);
if (!normalized) return;
if (ipaddr.isValid(normalized)) return ipaddr.parse(normalized);
return parseIpv6WithEmbeddedIpv4(normalized);
}
function isCanonicalDottedDecimalIPv4(raw) {
const trimmed = raw?.trim();
if (!trimmed) return false;
const normalized = stripIpv6Brackets(trimmed);
if (!normalized) return false;
return ipaddr.IPv4.isValidFourPartDecimal(normalized);
}
function isLegacyIpv4Literal(raw) {
const trimmed = raw?.trim();
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;
}
function isLoopbackIpAddress(raw) {
const parsed = parseCanonicalIpAddress(raw);
if (!parsed) return false;
return normalizeIpv4MappedAddress(parsed).range() === "loopback";
}
function isPrivateOrLoopbackIpAddress(raw) {
const parsed = parseCanonicalIpAddress(raw);
if (!parsed) return false;
const normalized = normalizeIpv4MappedAddress(parsed);
if (isIpv4Address(normalized)) return PRIVATE_OR_LOOPBACK_IPV4_RANGES.has(normalized.range());
if (PRIVATE_OR_LOOPBACK_IPV6_RANGES.has(normalized.range())) return true;
return (normalized.parts[0] & 65472) === 65216;
}
function isBlockedSpecialUseIpv4Address(address, options = {}) {
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, low) {
const octets = [
high >>> 8 & 255,
high & 255,
low >>> 8 & 255,
low & 255
];
return ipaddr.IPv4.parse(octets.join("."));
}
function extractEmbeddedIpv4FromIpv6(address) {
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);
}
}
//#endregion
//#region src/infra/net/hostname.ts
function normalizeHostname(hostname) {
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
if (normalized.startsWith("[") && normalized.endsWith("]")) return normalized.slice(1, -1);
return normalized;
}
//#endregion
//#region src/infra/net/ssrf.ts
var SsrFBlockedError = class extends Error {
constructor(message) {
super(message);
this.name = "SsrFBlockedError";
}
};
const BLOCKED_HOSTNAMES = new Set([
"localhost",
"localhost.localdomain",
"metadata.google.internal"
]);
function normalizeHostnameSet(values) {
if (!values || values.length === 0) return /* @__PURE__ */ new Set();
return new Set(values.map((value) => normalizeHostname(value)).filter(Boolean));
}
function normalizeHostnameAllowlist(values) {
if (!values || values.length === 0) return [];
return Array.from(new Set(values.map((value) => normalizeHostname(value)).filter((value) => value !== "*" && value !== "*." && value.length > 0)));
}
function resolveAllowPrivateNetwork(policy) {
return policy?.dangerouslyAllowPrivateNetwork === true || policy?.allowPrivateNetwork === true;
}
function resolveIpv4SpecialUseBlockOptions(policy) {
return { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange === true };
}
function isHostnameAllowedByPattern(hostname, pattern) {
if (pattern.startsWith("*.")) {
const suffix = pattern.slice(2);
if (!suffix || hostname === suffix) return false;
return hostname.endsWith(`.${suffix}`);
}
return hostname === pattern;
}
function matchesHostnameAllowlist(hostname, allowlist) {
if (allowlist.length === 0) return true;
return allowlist.some((pattern) => isHostnameAllowedByPattern(hostname, pattern));
}
function looksLikeUnsupportedIpv4Literal(address) {
const parts = address.split(".");
if (parts.length === 0 || parts.length > 4) return false;
if (parts.some((part) => part.length === 0)) return true;
return parts.every((part) => /^[0-9]+$/.test(part) || /^0x/i.test(part));
}
function isPrivateIpAddress(address, policy) {
let normalized = address.trim().toLowerCase();
if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
if (!normalized) return false;
const blockOptions = resolveIpv4SpecialUseBlockOptions(policy);
const strictIp = parseCanonicalIpAddress(normalized);
if (strictIp) {
if (isIpv4Address(strictIp)) return isBlockedSpecialUseIpv4Address(strictIp, blockOptions);
if (isPrivateOrLoopbackIpAddress(strictIp.toString())) return true;
const embeddedIpv4 = extractEmbeddedIpv4FromIpv6(strictIp);
if (embeddedIpv4) return isBlockedSpecialUseIpv4Address(embeddedIpv4, blockOptions);
return false;
}
if (normalized.includes(":") && !parseLooseIpAddress(normalized)) return true;
if (!isCanonicalDottedDecimalIPv4(normalized) && isLegacyIpv4Literal(normalized)) return true;
if (looksLikeUnsupportedIpv4Literal(normalized)) return true;
return false;
}
function isBlockedHostnameNormalized(normalized) {
if (BLOCKED_HOSTNAMES.has(normalized)) return true;
return normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal");
}
function isBlockedHostnameOrIp(hostname, policy) {
const normalized = normalizeHostname(hostname);
if (!normalized) return false;
return isBlockedHostnameNormalized(normalized) || isPrivateIpAddress(normalized, policy);
}
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, policy) {
if (isBlockedHostnameOrIp(hostnameOrIp, policy)) throw new SsrFBlockedError(BLOCKED_HOST_OR_IP_MESSAGE);
}
function assertAllowedResolvedAddressesOrThrow(results, policy) {
for (const entry of results) if (isBlockedHostnameOrIp(entry.address, policy)) throw new SsrFBlockedError(BLOCKED_RESOLVED_IP_MESSAGE);
}
function createPinnedLookup(params) {
const normalizedHost = normalizeHostname(params.hostname);
const fallback = params.fallback ?? lookup;
const fallbackLookup = fallback;
const fallbackWithOptions = fallback;
const records = params.addresses.map((address) => ({
address,
family: address.includes(":") ? 6 : 4
}));
let index = 0;
return ((host, options, callback) => {
const cb = typeof options === "function" ? options : callback;
if (!cb) return;
const normalized = normalizeHostname(host);
if (!normalized || normalized !== normalizedHost) {
if (typeof options === "function" || options === void 0) return fallbackLookup(host, cb);
return fallbackWithOptions(host, options, cb);
}
const opts = typeof options === "object" && options !== null ? options : {};
const requestedFamily = typeof options === "number" ? options : typeof opts.family === "number" ? opts.family : 0;
const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
const usable = candidates.length > 0 ? candidates : records;
if (opts.all) {
cb(null, usable);
return;
}
const chosen = usable[index % usable.length];
index += 1;
cb(null, chosen.address, chosen.family);
});
}
function dedupeAndPreferIpv4(results) {
const seen = /* @__PURE__ */ new Set();
const ipv4 = [];
const otherFamilies = [];
for (const entry of results) {
if (seen.has(entry.address)) continue;
seen.add(entry.address);
if (entry.family === 4) {
ipv4.push(entry.address);
continue;
}
otherFamilies.push(entry.address);
}
return [...ipv4, ...otherFamilies];
}
async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
const normalized = normalizeHostname(hostname);
if (!normalized) throw new Error("Invalid hostname");
const allowPrivateNetwork = resolveAllowPrivateNetwork(params.policy);
const allowedHostnames = normalizeHostnameSet(params.policy?.allowedHostnames);
const hostnameAllowlist = normalizeHostnameAllowlist(params.policy?.hostnameAllowlist);
const isExplicitAllowed = allowedHostnames.has(normalized);
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitAllowed;
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) throw new SsrFBlockedError(`Blocked hostname (not in allowlist): ${hostname}`);
if (!skipPrivateNetworkChecks) assertAllowedHostOrIpOrThrow(normalized, params.policy);
const results = await (params.lookupFn ?? lookup$1)(normalized, { all: true });
if (results.length === 0) throw new Error(`Unable to resolve hostname: ${hostname}`);
if (!skipPrivateNetworkChecks) assertAllowedResolvedAddressesOrThrow(results, params.policy);
const addresses = dedupeAndPreferIpv4(results);
if (addresses.length === 0) throw new Error(`Unable to resolve hostname: ${hostname}`);
return {
hostname: normalized,
addresses,
lookup: createPinnedLookup({
hostname: normalized,
addresses
})
};
}
function createPinnedDispatcher(pinned) {
return new Agent({ connect: {
lookup: pinned.lookup,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 300
} });
}
async function closeDispatcher(dispatcher) {
if (!dispatcher) return;
const candidate = dispatcher;
try {
if (typeof candidate.close === "function") {
await candidate.close();
return;
}
if (typeof candidate.destroy === "function") candidate.destroy();
} catch {}
}
//#endregion
export { resolvePinnedHostnameWithPolicy as a, isBlockedHostnameOrIp as i, closeDispatcher as n, normalizeHostname as o, createPinnedDispatcher as r, isLoopbackIpAddress as s, SsrFBlockedError as t };