omniroute / src /shared /network /outboundUrlGuard.ts
igorkurgin's picture
Deploy OmniRoute to Hugging Face Space
35743bd verified
Raw
History Blame Contribute Delete
3.87 kB
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";
}