chore(ssrf): include lookup error code in error message

This commit is contained in:
jubnl
2026-06-16 06:52:03 +02:00
parent bf969ee80d
commit 5b41cab898
+11 -9
View File
@@ -16,11 +16,11 @@ function isAlwaysBlocked(ip: string): boolean {
const addr = ip.startsWith('[') ? ip.slice(1, -1) : ip; const addr = ip.startsWith('[') ? ip.slice(1, -1) : ip;
// Loopback // Loopback
if (addr.startsWith("127.") || addr === '::1') return true; if (addr.startsWith('127.') || addr === '::1') return true;
// Unspecified // Unspecified
if (addr.startsWith("0.")) return true; if (addr.startsWith('0.')) return true;
// Link-local / cloud metadata // Link-local / cloud metadata
if (addr.startsWith("169.254.") || /^fe80:/i.test(addr)) return true; if (addr.startsWith('169.254.') || /^fe80:/i.test(addr)) return true;
// IPv4-mapped loopback / link-local: ::ffff:127.x.x.x, ::ffff:169.254.x.x // IPv4-mapped loopback / link-local: ::ffff:127.x.x.x, ::ffff:169.254.x.x
if (/^::ffff:127\./i.test(addr) || /^::ffff:169\.254\./i.test(addr)) return true; if (/^::ffff:127\./i.test(addr) || /^::ffff:169\.254\./i.test(addr)) return true;
@@ -32,9 +32,9 @@ function isPrivateNetwork(ip: string): boolean {
const addr = ip.startsWith('[') ? ip.slice(1, -1) : ip; const addr = ip.startsWith('[') ? ip.slice(1, -1) : ip;
// RFC-1918 private ranges // RFC-1918 private ranges
if (addr.startsWith("10.")) return true; if (addr.startsWith('10.')) return true;
if (/^172\.(1[6-9]|2\d|3[01])\./.test(addr)) return true; if (/^172\.(1[6-9]|2\d|3[01])\./.test(addr)) return true;
if (addr.startsWith("192.168.")) return true; if (addr.startsWith('192.168.')) return true;
// CGNAT / Tailscale shared address space (100.64.0.0/10) // CGNAT / Tailscale shared address space (100.64.0.0/10)
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(addr)) return true; if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(addr)) return true;
// IPv6 ULA (fc00::/7) // IPv6 ULA (fc00::/7)
@@ -71,8 +71,9 @@ export async function checkSsrf(rawUrl: string, bypassInternalIpAllowed: boolean
try { try {
const result = await dns.lookup(hostname); const result = await dns.lookup(hostname);
resolvedIp = result.address; resolvedIp = result.address;
} catch { } catch (error_) {
return { allowed: false, isPrivate: false, error: 'Could not resolve hostname' }; const code = error_ instanceof Error && 'code' in error_ ? String(error_.code) : 'unknown';
return { allowed: false, isPrivate: false, error: `Could not resolve hostname (${code})` };
} }
if (isAlwaysBlocked(resolvedIp)) { if (isAlwaysBlocked(resolvedIp)) {
@@ -90,7 +91,8 @@ export async function checkSsrf(rawUrl: string, bypassInternalIpAllowed: boolean
allowed: false, allowed: false,
isPrivate: true, isPrivate: true,
resolvedIp, resolvedIp,
error: 'Requests to private/internal network addresses are not allowed. Set ALLOW_INTERNAL_NETWORK=true to permit this for self-hosted setups.', error:
'Requests to private/internal network addresses are not allowed. Set ALLOW_INTERNAL_NETWORK=true to permit this for self-hosted setups.',
}; };
} }
return { allowed: true, isPrivate: true, resolvedIp }; return { allowed: true, isPrivate: true, resolvedIp };
@@ -187,7 +189,7 @@ export async function safeFetchFollow(
// (2xx/4xx/5xx, or a 3xx with no Location) is the final response. // (2xx/4xx/5xx, or a 3xx with no Location) is the final response.
const status = typeof response.status === 'number' ? response.status : 0; const status = typeof response.status === 'number' ? response.status : 0;
const isRedirectStatus = status >= 300 && status < 400; const isRedirectStatus = status >= 300 && status < 400;
const location = isRedirectStatus ? response.headers?.get('location') ?? null : null; const location = isRedirectStatus ? (response.headers?.get('location') ?? null) : null;
if (!location) { if (!location) {
return response; return response;
} }