import * as dnsPromises from "node:dns/promises"; import { isIP } from "node:net"; const PRIVATE_IP_RANGES: Array<{ start: bigint; end: bigint }> = [ // 10.0.0.0/8 { start: BigInt("0x0a000000"), end: BigInt("0x0affffff") }, // 172.16.0.0/12 { start: BigInt("0xac100000"), end: BigInt("0xac1fffff") }, // 192.168.0.0/16 { start: BigInt("0xc0a80000"), end: BigInt("0xc0a8ffff") }, // 127.0.0.0/8 (loopback) { start: BigInt("0x7f000000"), end: BigInt("0x7fffffff") }, // 169.254.0.0/16 (link-local) { start: BigInt("0xa9fe0000"), end: BigInt("0xa9feffff") }, // 0.0.0.0/8 { start: BigInt("0x00000000"), end: BigInt("0x00ffffff") }, // 100.64.0.0/10 (CGNAT) { start: BigInt("0x64400000"), end: BigInt("0x647fffff") }, ]; export function isPrivateIp(ip: string): boolean { const type = isIP(ip); if (type === 4) { const parts = ip.split(".").map(Number); const numeric = (BigInt(parts[0] ?? 0) << BigInt(24)) | (BigInt(parts[1] ?? 0) << BigInt(16)) | (BigInt(parts[2] ?? 0) << BigInt(8)) | BigInt(parts[3] ?? 0); return PRIVATE_IP_RANGES.some( (r) => numeric >= r.start && numeric <= r.end, ); } if (type === 6) { const lower = ip.toLowerCase(); if (lower === "::1") return true; if (lower.startsWith("fc") || lower.startsWith("fd")) return true; if (lower.startsWith("fe80")) return true; if (lower.startsWith("::ffff:")) { const v4 = lower.slice(7); return isPrivateIp(v4); } return false; } return false; } export function matchDomain(hostname: string, pattern: string): boolean { if (pattern === "*") return true; const lowerHost = hostname.toLowerCase(); const lowerPattern = pattern.toLowerCase(); if (lowerPattern.startsWith("*.")) { const suffix = lowerPattern.slice(2); return lowerHost === suffix || lowerHost.endsWith("." + suffix); } return lowerHost === lowerPattern; } export function checkDomainAccess( hostname: string, allowedDomains: string[], blockedDomains: string[], ): void { for (const blocked of blockedDomains) { if (matchDomain(hostname, blocked)) { throw new Error(`域名被黑名单禁止: ${hostname}`); } } // 白名单为空时允许所有域名(仅靠黑名单限制) if (allowedDomains.length > 0) { const allowed = allowedDomains.some((d) => matchDomain(hostname, d)); if (!allowed) { throw new Error(`域名不在白名单中: ${hostname}`); } } } export async function assertSafeUrl(url: string): Promise { let parsed: URL; try { parsed = new URL(url); } catch { throw new Error("无效的 URL 格式"); } if (!["http:", "https:"].includes(parsed.protocol)) { throw new Error("仅支持 http/https 协议"); } const hostname = parsed.hostname; if (!hostname) { throw new Error("URL 缺少主机名"); } // 如果 hostname 本身就是 IP,直接检查 if (isIP(hostname)) { if (isPrivateIp(hostname)) { throw new Error(`禁止访问内网地址: ${hostname}`); } return; } // DNS 解析,检查所有返回的 IP let addresses: { address: string; family: number }[]; try { addresses = await dnsPromises.lookup(hostname, { all: true }); } catch { throw new Error(`DNS 解析失败: ${hostname}`); } if (addresses.length === 0) { throw new Error(`DNS 解析无结果: ${hostname}`); } for (const addr of addresses) { if (isPrivateIp(addr.address)) { throw new Error(`禁止访问内网地址: ${addr.address} (${hostname})`); } } }