|
|
/**
|
|
|
* Guard CC API base URLs against classic SSRF (metadata / RFC1918).
|
|
|
* Allows official CarrierCentral hosts and localhost for local mock/dev.
|
|
|
* Hosts listed in `CC_API_BASE` env (or matching that hostname) also allowed.
|
|
|
*/
|
|
|
|
|
|
function isPrivateOrSpecialHost(hostname: string): boolean {
|
|
|
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
|
if (h === "localhost" || h === "127.0.0.1" || h === "::1") return false;
|
|
|
|
|
|
if (h === "0.0.0.0" || h === "169.254.169.254" || h === "metadata.google.internal") {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
// IPv4 private / link-local / loopback
|
|
|
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
|
if (m) {
|
|
|
const a = Number(m[1]);
|
|
|
const b = Number(m[2]);
|
|
|
if (a === 10) return true;
|
|
|
if (a === 127) return true;
|
|
|
if (a === 0) 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;
|
|
|
}
|
|
|
|
|
|
// IPv6 ULA / loopback / link-local
|
|
|
if (h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80")) return true;
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
function hostAllowedByDefault(hostname: string): boolean {
|
|
|
const h = hostname.toLowerCase();
|
|
|
if (h === "localhost" || h === "127.0.0.1" || h === "::1") return true;
|
|
|
if (h === "carriercentral.vip" || h.endsWith(".carriercentral.vip")) return true;
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
function hostMatchesEnvBase(hostname: string): boolean {
|
|
|
const raw = process.env.CC_API_BASE?.trim();
|
|
|
if (!raw) return false;
|
|
|
try {
|
|
|
return new URL(raw).hostname.toLowerCase() === hostname.toLowerCase();
|
|
|
} catch {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** @returns normalized URL string (no trailing slash required) */
|
|
|
export function assertSafeCcApiBase(urlStr: string): string {
|
|
|
let u: URL;
|
|
|
try {
|
|
|
u = new URL(urlStr.trim());
|
|
|
} catch {
|
|
|
throw new Error("CC_API_BASE_INVALID");
|
|
|
}
|
|
|
|
|
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
|
throw new Error("CC_API_BASE_PROTOCOL");
|
|
|
}
|
|
|
|
|
|
const host = u.hostname;
|
|
|
if (!host) throw new Error("CC_API_BASE_INVALID");
|
|
|
|
|
|
// Production: https only except localhost
|
|
|
const isLocal =
|
|
|
host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
|
if (process.env.NODE_ENV === "production" && u.protocol !== "https:" && !isLocal) {
|
|
|
throw new Error("CC_API_BASE_HTTPS_REQUIRED");
|
|
|
}
|
|
|
|
|
|
if (isPrivateOrSpecialHost(host) && !isLocal) {
|
|
|
throw new Error("CC_API_BASE_PRIVATE_HOST");
|
|
|
}
|
|
|
|
|
|
if (!hostAllowedByDefault(host) && !hostMatchesEnvBase(host)) {
|
|
|
throw new Error(
|
|
|
"CC_API_BASE_HOST_NOT_ALLOWED: 仅允许 *.carriercentral.vip、localhost,或与 CC_API_BASE 同主机",
|
|
|
);
|
|
|
}
|
|
|
|
|
|
return u.toString().replace(/\/$/, "");
|
|
|
}
|