修改了bug
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import { tool } from 'langchain';
|
||||
import * as z from 'zod';
|
||||
import { checkUrlAllowed } from './url_guard.js';
|
||||
|
||||
export const httpGetTool = tool(
|
||||
async ({ url, headers = {}, timeout = 30000 }) => {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (e) {
|
||||
return JSON.stringify({ success: false, error: `Invalid URL: ${url}` });
|
||||
// SSRF 防护:仅允许 http/https 且目标不得为环回/内网地址
|
||||
const blocked = checkUrlAllowed(url);
|
||||
if (blocked) {
|
||||
return JSON.stringify({ success: false, error: blocked });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import z from "zod";
|
||||
import { checkUrlAllowed } from "./url_guard.js";
|
||||
|
||||
const httpPostTool = tool(
|
||||
async ({ url, body, headers = {}, contentType = 'json', timeout = 30000 }) => {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (e) {
|
||||
return { success: false, error: `Invalid URL: ${url}` };
|
||||
// SSRF 防护:仅允许 http/https 且目标不得为环回/内网地址
|
||||
const blocked = checkUrlAllowed(url);
|
||||
if (blocked) {
|
||||
return { success: false, error: blocked };
|
||||
}
|
||||
|
||||
let requestBody;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 出网 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;
|
||||
}
|
||||
Reference in New Issue
Block a user