You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

86 lines
2.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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(/\/$/, "");
}