/** * 出网 URL 安全校验(SSRF 防护) * * Agent 的 HTTP 工具处理的 URL 来自模型输出,而模型输入含不可信的用户内容, * 因此可能被诱导去访问环回/内网地址(如本机 127.0.0.1 上的其他服务、云元数据地址)。 * 这里集中做白名单式校验:仅允许 http/https,且目标不能是环回/内网/链路本地地址。 */ // 仅允许的协议 const ALLOWED_PROTOCOLS = ["http:", "https:"]; // 本地主机名 const BLOCKED_HOSTNAMES = new Set(["localhost", "localhost.localdomain", "ip6-localhost"]); /** * 是否为内网/保留 IPv4 地址 * @param {string} host 形如 10.0.0.1 的地址 */ function isPrivateIPv4(host) { const parts = host.split("."); if (parts.length !== 4) return false; const nums = parts.map((p) => (/^\d+$/.test(p) ? Number(p) : NaN)); if (nums.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return false; const [a, b] = nums; if (a === 0 || a === 10 || a === 127) return true; // 当前网络/内网/环回 if (a === 100 && b >= 64 && b <= 127) return true; // 100.64.0.0/10 运营商级 NAT if (a === 169 && b === 254) return true; // 169.254.0.0/16 链路本地(含云 metadata) if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12 if (a === 192 && b === 168) return true; // 192.168.0.0/16 return false; } /** * 校验 URL 是否允许出网访问 * @param {string} url 待校验的 URL * @returns {string|null} 不允许时返回原因文案,允许时返回 null */ export function checkUrlAllowed(url) { let parsed; try { parsed = new URL(url); } catch (e) { return `Invalid URL: ${url}`; } if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) { return `Blocked protocol: ${parsed.protocol}`; } // URL.hostname 对 IPv6 会带方括号,统一去掉并转小写 const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase(); if (BLOCKED_HOSTNAMES.has(hostname)) { return `Blocked host: ${hostname}`; } if (isPrivateIPv4(hostname)) { return `Blocked private network address: ${hostname}`; } if (hostname.includes(":")) { // IPv6:环回 ::1 / 未指定 :: / 唯一本地 fc00::、fd00:: / 链路本地 fe80:: if (hostname === "::1" || hostname === "::") { return `Blocked private network address: ${hostname}`; } if (/^f[cd]/.test(hostname)) { return `Blocked private network address: ${hostname}`; } if (/^fe[89ab]/.test(hostname)) { return `Blocked private network address: ${hostname}`; } } return null; }