Files
api_health/agent/infra/tools/web/url_guard.js
T
2026-10-07 15:51:13 +08:00

75 lines
2.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 出网 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;
}