Commit eb9df086 by Archer Committed by GitHub

perf: SSRF check (#6805)

* perf: SSRF check

* doc
parent a5cd8229
...@@ -16,3 +16,4 @@ description: 'FastGPT V4.14.14 更新说明' ...@@ -16,3 +16,4 @@ description: 'FastGPT V4.14.14 更新说明'
1. 个人微信发布渠道,优化轮询策略(拉取与回复解耦),避免数据量超大时出现阻塞。 1. 个人微信发布渠道,优化轮询策略(拉取与回复解耦),避免数据量超大时出现阻塞。
2. 新增环境变量 `WECHAT_CHANNEL_CONCURRENCY`(默认 1000)用于控制微信渠道 poll worker 并发数,建议 ≥ online channel 峰值。 2. 新增环境变量 `WECHAT_CHANNEL_CONCURRENCY`(默认 1000)用于控制微信渠道 poll worker 并发数,建议 ≥ online channel 峰值。
3. 完善内网地址检测。
\ No newline at end of file
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
"document/content/docs/self-host/upgrading/4-14/41412.mdx": "2026-04-21T23:04:26+08:00", "document/content/docs/self-host/upgrading/4-14/41412.mdx": "2026-04-21T23:04:26+08:00",
"document/content/docs/self-host/upgrading/4-14/41413.en.mdx": "2026-04-21T23:04:26+08:00", "document/content/docs/self-host/upgrading/4-14/41413.en.mdx": "2026-04-21T23:04:26+08:00",
"document/content/docs/self-host/upgrading/4-14/41413.mdx": "2026-04-21T23:04:26+08:00", "document/content/docs/self-host/upgrading/4-14/41413.mdx": "2026-04-21T23:04:26+08:00",
"document/content/docs/self-host/upgrading/4-14/41414.mdx": "2026-04-22T16:09:29+08:00", "document/content/docs/self-host/upgrading/4-14/41414.mdx": "2026-04-22T16:29:40+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00",
...@@ -393,7 +393,7 @@ ...@@ -393,7 +393,7 @@
"document/content/docs/self-host/upgrading/upgrade-intruction.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/upgrade-intruction.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/upgrade-intruction.mdx": "2026-04-20T13:51:34+08:00", "document/content/docs/self-host/upgrading/upgrade-intruction.mdx": "2026-04-20T13:51:34+08:00",
"document/content/docs/toc.en.mdx": "2026-04-21T23:04:26+08:00", "document/content/docs/toc.en.mdx": "2026-04-21T23:04:26+08:00",
"document/content/docs/toc.mdx": "2026-04-22T16:09:29+08:00", "document/content/docs/toc.mdx": "2026-04-22T16:29:40+08:00",
"document/content/docs/use-cases/app-cases/dalle3.en.mdx": "2026-02-26T22:14:30+08:00", "document/content/docs/use-cases/app-cases/dalle3.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/use-cases/app-cases/dalle3.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/app-cases/dalle3.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-02-26T22:14:30+08:00", "document/content/docs/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-02-26T22:14:30+08:00",
......
import { isIP, isIPv6 } from 'net'; import ipaddr from 'ipaddr.js';
import { isIPv6 } from 'net';
import dns from 'dns/promises'; import dns from 'dns/promises';
const isDevEnv = process.env.NODE_ENV === 'development'; const isDevEnv = process.env.NODE_ENV === 'development';
...@@ -8,188 +9,172 @@ const SERVICE_LOCAL_HOST = ...@@ -8,188 +9,172 @@ const SERVICE_LOCAL_HOST =
? `[${process.env.HOSTNAME}]:${SERVICE_LOCAL_PORT}` ? `[${process.env.HOSTNAME}]:${SERVICE_LOCAL_PORT}`
: `${process.env.HOSTNAME || 'localhost'}:${SERVICE_LOCAL_PORT}`; : `${process.env.HOSTNAME || 'localhost'}:${SERVICE_LOCAL_PORT}`;
export const isInternalAddress = async (url: string): Promise<boolean> => { // 云厂商元数据服务 IP(除 169.254.0.0/16 段外的特殊地址)
if (isDevEnv) return false; // 预先归一化为 ipaddr.js 的 normalizedString 形式以便比对
const METADATA_IPS = new Set<string>(
const isInternalIPv6 = (ip: string): boolean => { [
// 移除 IPv6 地址中的方括号(如果有) '100.100.100.200', // 阿里云
const cleanIp = ip.replace(/^\[|\]$/g, ''); 'fd00:ec2::254' // AWS IPv6
].map((ip) => ipaddr.parse(ip).toNormalizedString().toLowerCase())
// 检查 IPv4-mapped IPv6 地址(格式:::ffff:xxxx:xxxx) );
// Node.js URL 解析器会将 IPv4 部分转换为十六进制
const ipv4MappedPattern = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i; // 云厂商元数据服务主机名(归一化:小写、去尾部点)
const ipv4MappedMatch = cleanIp.match(ipv4MappedPattern); const METADATA_HOSTNAMES = new Set<string>([
'metadata.google.internal',
'metadata',
'metadata.tencentyun.com',
'kubernetes.default.svc',
'kubernetes.default',
'kubernetes'
]);
const LOCALHOST_HOSTNAMES = new Set<string>(['localhost']);
/**
* 把 URL hostname 尝试解析成 ipaddr.js 的地址对象
* - 处理 IPv6 方括号
* - 处理 IPv4-mapped IPv6 (::ffff:a.b.c.d / ::ffff:xxxx:xxxx) → 解包为 IPv4
* - 处理十进制/十六进制/八进制/短点分 IPv4 字面量
* 非 IP 字面量返回 null
*/
const parseHostAsIP = (rawHostname: string): ipaddr.IPv4 | ipaddr.IPv6 | null => {
const host = rawHostname.replace(/^\[|\]$/g, '').replace(/\.$/, '');
if (!host) return null;
// ipaddr.process 会自动把 IPv4-mapped IPv6 解包为 IPv4,处理常规字面量
if (ipaddr.isValid(host)) {
try {
return ipaddr.process(host);
} catch {
return null;
}
}
if (ipv4MappedMatch) { // ipaddr.js 不支持十进制/十六进制/八进制 IPv4 短写,手动兜底
// 将十六进制转换回 IPv4 地址 const numeric = parseNumericIPv4(host);
const hex1 = parseInt(ipv4MappedMatch[1], 16); if (numeric) return ipaddr.parse(numeric) as ipaddr.IPv4;
const hex2 = parseInt(ipv4MappedMatch[2], 16);
// hex1 包含前两个字节,hex2 包含后两个字节 return null;
const byte1 = (hex1 >> 8) & 0xff; };
const byte2 = hex1 & 0xff;
const byte3 = (hex2 >> 8) & 0xff;
const byte4 = hex2 & 0xff;
const ipv4 = `${byte1}.${byte2}.${byte3}.${byte4}`; /**
return isInternalIPv4(ipv4); * 解析 inet_aton 兼容的 IPv4 字面量:十进制 2852039166、十六进制 0xa9fea9fe、
* 八进制、1-4 段形式(含 dec/hex/oct 混合)。返回标准点分十进制或 null
*/
const parseNumericIPv4 = (host: string): string | null => {
const parts = host.split('.');
if (parts.length === 0 || parts.length > 4) return null;
const nums: number[] = [];
for (const part of parts) {
if (!part) return null;
let n: number;
if (/^0x[0-9a-f]+$/i.test(part)) n = parseInt(part, 16);
else if (/^0[0-7]+$/.test(part)) n = parseInt(part, 8);
else if (/^\d+$/.test(part)) n = parseInt(part, 10);
else return null;
if (!Number.isFinite(n) || n < 0) return null;
nums.push(n);
} }
// IPv6 内部地址范围 const maxLast = [0xffffffff, 0xffffff, 0xffff, 0xff][parts.length - 1];
const internalIPv6Patterns = [ if (nums[nums.length - 1] > maxLast) return null;
/^::1$/, // Loopback for (let i = 0; i < nums.length - 1; i++) if (nums[i] > 0xff) return null;
/^::$/, // Unspecified
/^fe80:/i, // Link-local
/^fc00:/i, // Unique local address (ULA)
/^fd00:/i, // Unique local address (ULA)
/^::ffff:0:0/i, // IPv4-mapped IPv6
/^::ffff:127\./i, // IPv4-mapped loopback
/^::ffff:10\./i, // IPv4-mapped private (10.0.0.0/8)
/^::ffff:172\.(1[6-9]|2[0-9]|3[0-1])\./i, // IPv4-mapped private (172.16.0.0/12)
/^::ffff:192\.168\./i, // IPv4-mapped private (192.168.0.0/16)
/^::ffff:169\.254\./i, // IPv4-mapped link-local
/^::ffff:100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./i // IPv4-mapped shared address space
];
return internalIPv6Patterns.some((pattern) => pattern.test(cleanIp)); let ipInt = 0;
}; for (let i = 0; i < nums.length - 1; i++) ipInt = (ipInt + nums[i]) * 256;
const isInternalIPv4 = (ip: string): boolean => { ipInt += nums[nums.length - 1];
// 验证是否为有效的 IPv4 格式 if (ipInt > 0xffffffff) return null;
const ipv4Pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
const match = ip.match(ipv4Pattern);
if (!match) { return [(ipInt >>> 24) & 0xff, (ipInt >>> 16) & 0xff, (ipInt >>> 8) & 0xff, ipInt & 0xff].join(
return false; '.'
} );
};
// 解析 IP 地址的各个部分 const normalizeDomain = (rawHostname: string): string =>
const parts = [ rawHostname
parseInt(match[1], 10), .replace(/^\[|\]$/g, '')
parseInt(match[2], 10), .replace(/\.$/, '')
parseInt(match[3], 10), .toLowerCase();
parseInt(match[4], 10)
]; /**
* ipaddr.js range() 返回的所有非 'unicast' 分类都视为内部地址。
* 主要范围:private / loopback / linkLocal / uniqueLocal / reserved /
* multicast / broadcast / unspecified / carrierGradeNat 等
*/
const isInternalIPAddress = (addr: ipaddr.IPv4 | ipaddr.IPv6): boolean => {
return addr.range() !== 'unicast';
};
// 验证每个部分是否在有效范围内 (0-255) /**
if (parts.some((part) => part < 0 || part > 255)) { * 元数据端点:
return false; * - 169.254.0.0/16 link-local 段全部视为元数据
} * - 显式列表里的 IP(阿里云 100.100.100.200、AWS IPv6 fd00:ec2::254)
*/
const isMetadataIPAddress = (addr: ipaddr.IPv4 | ipaddr.IPv6): boolean => {
if (addr.kind() === 'ipv4' && addr.range() === 'linkLocal') return true;
return METADATA_IPS.has(addr.toNormalizedString().toLowerCase());
};
// 检查是否为内部 IP 地址范围 export const isInternalAddress = async (url: string): Promise<boolean> => {
return ( if (isDevEnv) return false;
parts[0] === 0 || // 0.0.0.0/8 - Current network
parts[0] === 10 || // 10.0.0.0/8 - Private network
parts[0] === 127 || // 127.0.0.0/8 - Loopback
(parts[0] === 169 && parts[1] === 254) || // 169.254.0.0/16 - Link-local
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || // 172.16.0.0/12 - Private network
(parts[0] === 192 && parts[1] === 168) || // 192.168.0.0/16 - Private network
(parts[0] >= 224 && parts[0] <= 239) || // 224.0.0.0/4 - Multicast
(parts[0] >= 240 && parts[0] <= 255) || // 240.0.0.0/4 - Reserved
(parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) || // 100.64.0.0/10 - Shared address space
(parts[0] === 192 && parts[1] === 0 && parts[2] === 0) || // 192.0.0.0/24 - IETF Protocol Assignments
(parts[0] === 192 && parts[1] === 0 && parts[2] === 2) || // 192.0.2.0/24 - Documentation (TEST-NET-1)
(parts[0] === 198 && parts[1] === 18) || // 198.18.0.0/15 - Benchmarking
(parts[0] === 198 && parts[1] === 19) || // 198.18.0.0/15 - Benchmarking
(parts[0] === 198 && parts[1] === 51 && parts[2] === 100) || // 198.51.100.0/24 - Documentation (TEST-NET-2)
(parts[0] === 203 && parts[1] === 0 && parts[2] === 113) // 203.0.113.0/24 - Documentation (TEST-NET-3)
);
};
let parsedUrl: URL;
try { try {
const parsedUrl = new URL(url); parsedUrl = new URL(url);
// 移除 IPv6 地址的方括号(如果有) } catch {
const hostname = parsedUrl.hostname.replace(/^\[|\]$/g, ''); return false;
const fullUrl = parsedUrl.toString(); }
// 1. 检查 localhost 和常见的本地域名变体 const hostDomain = normalizeDomain(parsedUrl.hostname);
const localhostVariants = ['localhost', '127.0.0.1', '::1', '0.0.0.0']; const localHost = SERVICE_LOCAL_HOST.split(':')[0].toLowerCase();
const localHostname = SERVICE_LOCAL_HOST.split(':')[0];
if (localhostVariants.includes(hostname) || hostname === localHostname) { // 1. localhost / 本机
if (LOCALHOST_HOSTNAMES.has(hostDomain) || hostDomain === localHost) {
return true; return true;
} }
// 2. 检查云服务商元数据端点(始终阻止,无论 CHECK_INTERNAL_IP 设置如何) // 2. 云元数据主机名
const metadataEndpoints = [ if (METADATA_HOSTNAMES.has(hostDomain)) {
// AWS
'http://169.254.169.254/',
'http://[fd00:ec2::254]/',
// Azure
'http://169.254.169.254/',
// GCP
'http://metadata.google.internal/',
'http://metadata/',
// Alibaba Cloud
'http://100.100.100.200/',
// Tencent Cloud
'http://metadata.tencentyun.com/',
// Huawei Cloud
'http://169.254.169.254/',
// Oracle Cloud
'http://169.254.169.254/',
// DigitalOcean
'http://169.254.169.254/',
// Kubernetes
'http://kubernetes.default.svc/',
'https://kubernetes.default.svc/'
];
if (metadataEndpoints.some((endpoint) => fullUrl.startsWith(endpoint))) {
return true; return true;
} }
// 3. 只有显式设置 CHECK_INTERNAL_IP=true 时才启用私有 IP 检查 // 3. IP 字面量(含各种编码变体)
if (process.env.CHECK_INTERNAL_IP !== 'true') { const ip = parseHostAsIP(parsedUrl.hostname);
const checkFullInternal = process.env.CHECK_INTERNAL_IP === 'true';
if (ip) {
if (isMetadataIPAddress(ip)) return true;
// loopback/unspecified 等始终阻止(这些是显而易见的错误配置或攻击)
const range = ip.range();
if (range === 'loopback' || range === 'unspecified') return true;
if (checkFullInternal) return isInternalIPAddress(ip);
return false; return false;
} }
// 4. 使用 Node.js 的 isIP 函数检测 IP 版本 // 4. 域名:解析 DNS;元数据命中始终阻止,私有段受 CHECK_INTERNAL_IP 控制
const ipVersion = isIP(hostname);
if (ipVersion === 4) {
// IPv4 地址检查
return isInternalIPv4(hostname);
} else if (ipVersion === 6) {
// IPv6 地址检查
return isInternalIPv6(hostname);
} else {
// 不是 IP 地址,是域名 - 需要解析
try { try {
// 解析所有 A 和 AAAA 记录 const [v4Res, v6Res] = await Promise.allSettled([
const [ipv4Addresses, ipv6Addresses] = await Promise.allSettled([ dns.resolve4(hostDomain),
dns.resolve4(hostname), dns.resolve6(hostDomain)
dns.resolve6(hostname)
]); ]);
const resolvedIPs = [
// 检查所有解析的 IP 是否为内部地址 ...(v4Res.status === 'fulfilled' ? v4Res.value : []),
const allIPs = [ ...(v6Res.status === 'fulfilled' ? v6Res.value : [])
...(ipv4Addresses.status === 'fulfilled' ? ipv4Addresses.value : []),
...(ipv6Addresses.status === 'fulfilled' ? ipv6Addresses.value : [])
]; ];
// 如果任何一个解析的 IP 是内部地址,则拒绝 for (const raw of resolvedIPs) {
for (const ip of allIPs) { if (!ipaddr.isValid(raw)) continue;
if (isInternalIPv4(ip) || isInternalIPv6(ip)) { const addr = ipaddr.process(raw);
return true; if (isMetadataIPAddress(addr)) return true;
} const r = addr.range();
if (r === 'loopback' || r === 'unspecified') return true;
if (checkFullInternal && isInternalIPAddress(addr)) return true;
} }
return false; return false;
} catch (error) { } catch {
return false;
}
}
} catch (error) {
// URL 解析失败 - 宽松策略:允许访问
return false; return false;
} }
}; };
export const PRIVATE_URL_TEXT = 'Request to private network not allowed'; export const PRIVATE_URL_TEXT = 'Request to private network not allowed';
...@@ -8,12 +8,12 @@ ...@@ -8,12 +8,12 @@
}, },
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.2", "@apidevtools/json-schema-ref-parser": "^11.7.2",
"@mariozechner/pi-agent-core": "^0.67.3",
"@mariozechner/pi-ai": "^0.67.3",
"@fastgpt-sdk/sandbox-adapter": "^0.0.36",
"@fastgpt-sdk/otel": "catalog:", "@fastgpt-sdk/otel": "catalog:",
"@fastgpt-sdk/sandbox-adapter": "^0.0.36",
"@fastgpt-sdk/storage": "catalog:", "@fastgpt-sdk/storage": "catalog:",
"@fastgpt/global": "workspace:*", "@fastgpt/global": "workspace:*",
"@mariozechner/pi-agent-core": "^0.67.3",
"@mariozechner/pi-ai": "^0.67.3",
"@maxmind/geoip2-node": "^6.3.4", "@maxmind/geoip2-node": "^6.3.4",
"@modelcontextprotocol/sdk": "catalog:", "@modelcontextprotocol/sdk": "catalog:",
"@node-rs/jieba": "2.0.1", "@node-rs/jieba": "2.0.1",
...@@ -37,12 +37,13 @@ ...@@ -37,12 +37,13 @@
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"ioredis": "^5.6.0", "ioredis": "^5.6.0",
"ipaddr.js": "^2.3.0",
"joplin-turndown-plugin-gfm": "^1.0.12", "joplin-turndown-plugin-gfm": "^1.0.12",
"json5": "catalog:", "json5": "catalog:",
"jsonrepair": "^3.0.0",
"jszip": "^3.10.1",
"jsonpath-plus": "^10.3.0", "jsonpath-plus": "^10.3.0",
"jsonrepair": "^3.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"lodash": "catalog:", "lodash": "catalog:",
"mammoth": "^1.11.0", "mammoth": "^1.11.0",
"mime": "catalog:", "mime": "catalog:",
......
...@@ -345,6 +345,9 @@ importers: ...@@ -345,6 +345,9 @@ importers:
ioredis: ioredis:
specifier: ^5.6.0 specifier: ^5.6.0
version: 5.6.0 version: 5.6.0
ipaddr.js:
specifier: ^2.3.0
version: 2.3.0
joplin-turndown-plugin-gfm: joplin-turndown-plugin-gfm:
specifier: ^1.0.12 specifier: ^1.0.12
version: 1.0.12 version: 1.0.12
...@@ -8141,8 +8144,8 @@ packages: ...@@ -8141,8 +8144,8 @@ packages:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
ipaddr.js@2.2.0: ipaddr.js@2.3.0:
resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
is-absolute-url@4.0.1: is-absolute-url@4.0.1:
...@@ -21079,7 +21082,7 @@ snapshots: ...@@ -21079,7 +21082,7 @@ snapshots:
ipaddr.js@1.9.1: {} ipaddr.js@1.9.1: {}
ipaddr.js@2.2.0: {} ipaddr.js@2.3.0: {}
is-absolute-url@4.0.1: {} is-absolute-url@4.0.1: {}
...@@ -22450,7 +22453,7 @@ snapshots: ...@@ -22450,7 +22453,7 @@ snapshots:
buffer-crc32: 1.0.0 buffer-crc32: 1.0.0
eventemitter3: 5.0.1 eventemitter3: 5.0.1
fast-xml-parser: 5.4.2 fast-xml-parser: 5.4.2
ipaddr.js: 2.2.0 ipaddr.js: 2.3.0
lodash: 4.17.23 lodash: 4.17.23
mime-types: 2.1.35 mime-types: 2.1.35
query-string: 7.1.3 query-string: 7.1.3
......
...@@ -450,4 +450,83 @@ describe('SSRF Protection - isInternalAddress', () => { ...@@ -450,4 +450,83 @@ describe('SSRF Protection - isInternalAddress', () => {
expect(await isInternalAddress('http://dual-stack-public.example.com/')).toBe(false); expect(await isInternalAddress('http://dual-stack-public.example.com/')).toBe(false);
}); });
}); });
// GHSA-jhqw-944x-xh94: 云元数据端点 SSRF 保护绕过
describe('GHSA-jhqw-944x-xh94 元数据端点绕过防护', () => {
beforeEach(() => {
delete process.env.CHECK_INTERNAL_IP;
});
test('应该阻止显式端口绕过 http://169.254.169.254:80/', async () => {
expect(await isInternalAddress('http://169.254.169.254:80/latest/meta-data/')).toBe(true);
expect(await isInternalAddress('http://169.254.169.254:80/')).toBe(true);
});
test('应该阻止 IPv6-mapped IPv4 绕过 http://[::ffff:a9fe:a9fe]/', async () => {
expect(await isInternalAddress('http://[::ffff:a9fe:a9fe]/latest/meta-data/')).toBe(true);
expect(await isInternalAddress('http://[::ffff:169.254.169.254]/')).toBe(true);
});
test('应该阻止十六进制 IP 绕过 http://0xa9fea9fe/', async () => {
expect(await isInternalAddress('http://0xa9fea9fe/latest/meta-data/')).toBe(true);
expect(await isInternalAddress('http://0xA9FEA9FE/')).toBe(true);
});
test('应该阻止十进制 IP 绕过 http://2852039166/', async () => {
expect(await isInternalAddress('http://2852039166/latest/meta-data/')).toBe(true);
expect(await isInternalAddress('http://2852039166/')).toBe(true);
});
test('应该阻止尾部点绕过 http://169.254.169.254./', async () => {
expect(await isInternalAddress('http://169.254.169.254./latest/meta-data/')).toBe(true);
expect(await isInternalAddress('http://169.254.169.254./')).toBe(true);
});
test('应该阻止 nip.io 通配 DNS 绕过', async () => {
vi.mocked(dns.resolve4).mockResolvedValue(['169.254.169.254']);
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
expect(await isInternalAddress('http://169.254.169.254.nip.io/latest/meta-data/')).toBe(true);
});
test('应该阻止链路本地 /16 段内其他 IP', async () => {
expect(await isInternalAddress('http://169.254.1.1/')).toBe(true);
expect(await isInternalAddress('http://169.254.254.254:8080/')).toBe(true);
});
test('应该阻止 Alibaba Cloud 元数据 IP', async () => {
expect(await isInternalAddress('http://100.100.100.200:80/')).toBe(true);
});
test('应该阻止 AWS IPv6 元数据端点', async () => {
expect(await isInternalAddress('http://[fd00:ec2::254]/')).toBe(true);
});
test('应该阻止十进制/十六进制绕过阿里云元数据', async () => {
// 100.100.100.200 -> 0x64646464 -> 1684300900... 实际: 100*256^3+100*256^2+100*256+200
const decimal = 100 * 256 ** 3 + 100 * 256 ** 2 + 100 * 256 + 200;
expect(await isInternalAddress(`http://${decimal}/`)).toBe(true);
expect(await isInternalAddress(`http://0x${decimal.toString(16)}/`)).toBe(true);
});
test('应该阻止元数据主机名的尾部点/大小写变体', async () => {
expect(await isInternalAddress('http://METADATA.google.internal/')).toBe(true);
expect(await isInternalAddress('http://metadata.google.internal./')).toBe(true);
expect(await isInternalAddress('http://kubernetes.default.svc./')).toBe(true);
});
test('域名解析到阿里云元数据 IP 时始终阻止', async () => {
vi.mocked(dns.resolve4).mockResolvedValue(['100.100.100.200']);
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
expect(await isInternalAddress('http://rebind.example.com/')).toBe(true);
});
test('域名解析到 AWS 元数据 IP 时始终阻止(无需 CHECK_INTERNAL_IP)', async () => {
vi.mocked(dns.resolve4).mockResolvedValue(['169.254.169.254']);
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
expect(await isInternalAddress('http://metadata-proxy.example.com/')).toBe(true);
});
});
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment