75 lines
2.5 KiB
JavaScript
75 lines
2.5 KiB
JavaScript
/**
|
||
* 出网 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;
|
||
}
|