import { isIP } from "node:net"; const TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]); export const PROVIDER_URL_BLOCKED_MESSAGE = "Blocked private or local provider URL"; export const PRIVATE_PROVIDER_URLS_ENV = "OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS"; export type OutboundUrlGuardMode = "none" | "public-only"; export type OutboundUrlGuardErrorCode = "OUTBOUND_URL_GUARD_BLOCKED" | "OUTBOUND_URL_INVALID"; type OutboundUrlGuardErrorInit = { code: OutboundUrlGuardErrorCode; url: string; hostname?: string | null; }; export class OutboundUrlGuardError extends Error { code: OutboundUrlGuardErrorCode; url: string; hostname?: string | null; constructor(message: string, init: OutboundUrlGuardErrorInit) { super(message); this.name = "OutboundUrlGuardError"; this.code = init.code; this.url = init.url; this.hostname = init.hostname ?? null; } } function normalizeHost(hostname: string) { const normalized = hostname.trim().toLowerCase(); if (normalized.startsWith("[") && normalized.endsWith("]")) { return normalized.slice(1, -1); } return normalized; } export function isPrivateHost(hostname: string) { const normalized = normalizeHost(hostname); if (!normalized) return true; if ( normalized === "localhost" || normalized === "0.0.0.0" || normalized === "127.0.0.1" || normalized === "::1" || normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.startsWith("::ffff:") ) { return true; } if (isIP(normalized) === 4) { const octets = normalized.split(".").map((segment) => parseInt(segment, 10)); const [a, b] = octets; if (a === 0 || a === 10 || a === 127) return true; if (a === 169 && b === 254) return true; if (a === 192 && b === 168) return true; if (a === 172 && b >= 16 && b <= 31) return true; if (a === 100 && b >= 64 && b <= 127) return true; return false; } if (isIP(normalized) === 6) { return ( normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:") ); } return false; } export function parseOutboundUrl(input: string | URL) { let url: URL; try { url = input instanceof URL ? input : new URL(String(input)); } catch { throw new OutboundUrlGuardError(`Invalid outbound URL: ${String(input)}`, { code: "OUTBOUND_URL_INVALID", url: String(input), }); } if (url.protocol !== "http:" && url.protocol !== "https:") { throw new OutboundUrlGuardError(`Invalid outbound URL protocol for ${url.toString()}`, { code: "OUTBOUND_URL_INVALID", url: url.toString(), hostname: url.hostname || null, }); } if (url.username || url.password) { throw new OutboundUrlGuardError("Blocked outbound URL with embedded credentials", { code: "OUTBOUND_URL_GUARD_BLOCKED", url: url.toString(), hostname: url.hostname || null, }); } return url; } export function parseAndValidatePublicUrl(input: string | URL) { const url = parseOutboundUrl(input); if (isPrivateHost(url.hostname)) { throw new OutboundUrlGuardError(PROVIDER_URL_BLOCKED_MESSAGE, { code: "OUTBOUND_URL_GUARD_BLOCKED", url: url.toString(), hostname: url.hostname || null, }); } return url; } export function arePrivateProviderUrlsAllowed() { const value = process.env[PRIVATE_PROVIDER_URLS_ENV]; if (value && TRUE_ENV_VALUES.has(value.trim().toLowerCase())) return true; const legacyValue = process.env["OUTBOUND_SSRF_GUARD_ENABLED"]; if (legacyValue && ["false", "0", "no", "off"].includes(legacyValue.trim().toLowerCase())) return true; return false; } export function getProviderOutboundGuard(): OutboundUrlGuardMode { return arePrivateProviderUrlsAllowed() ? "none" : "public-only"; }