Spaces:
Paused
Paused
| 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"; | |
| } | |