From 5b41cab898cf2bbde2edf5622717c4810021146c Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 16 Jun 2026 06:52:03 +0200 Subject: [PATCH] chore(ssrf): include lookup error code in error message --- server/src/utils/ssrfGuard.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/server/src/utils/ssrfGuard.ts b/server/src/utils/ssrfGuard.ts index c7b27e82..e2f57665 100644 --- a/server/src/utils/ssrfGuard.ts +++ b/server/src/utils/ssrfGuard.ts @@ -16,11 +16,11 @@ function isAlwaysBlocked(ip: string): boolean { const addr = ip.startsWith('[') ? ip.slice(1, -1) : ip; // Loopback - if (addr.startsWith("127.") || addr === '::1') return true; + if (addr.startsWith('127.') || addr === '::1') return true; // Unspecified - if (addr.startsWith("0.")) return true; + if (addr.startsWith('0.')) return true; // 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 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; // 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 (addr.startsWith("192.168.")) return true; + if (addr.startsWith('192.168.')) return true; // 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; // IPv6 ULA (fc00::/7) @@ -71,8 +71,9 @@ export async function checkSsrf(rawUrl: string, bypassInternalIpAllowed: boolean try { const result = await dns.lookup(hostname); resolvedIp = result.address; - } catch { - return { allowed: false, isPrivate: false, error: 'Could not resolve hostname' }; + } catch (error_) { + 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)) { @@ -90,7 +91,8 @@ export async function checkSsrf(rawUrl: string, bypassInternalIpAllowed: boolean allowed: false, isPrivate: true, 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 }; @@ -187,7 +189,7 @@ export async function safeFetchFollow( // (2xx/4xx/5xx, or a 3xx with no Location) is the final response. const status = typeof response.status === 'number' ? response.status : 0; 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) { return response; }