{"record":{"id":"9c8d16c4c46db73b","repo":"mastra-ai/mastra","slug":"url-resolves-to-a-private-or-reserved-address","errorCode":null,"errorMessage":"URL resolves to a private or reserved address.","messagePattern":"URL resolves to a private or reserved address\\.","errorType":"exception","errorClass":"WebFetchError","httpStatus":null,"severity":"error","filePath":"packages/core/src/tools/builtin/web-fetch.ts","lineNumber":132,"sourceCode":"    (first & 0xff00) === 0xff00\n  );\n}\n\nfunction isBlockedIp(address: string): boolean {\n  const normalizedAddress = normalizeHostname(address);\n  const ipVersion = net.isIP(normalizedAddress);\n  return ipVersion === 4\n    ? isBlockedIpv4(normalizedAddress)\n    : ipVersion === 6\n      ? isBlockedIpv6(normalizedAddress)\n      : false;\n}\n\nfunction assertAllowedUrl(url: URL): void {\n  const hostname = normalizeHostname(url.hostname);\n\n  if (isBlockedHostname(hostname) || isBlockedIp(hostname)) {\n    throw new WebFetchError('URL resolves to a private or reserved address.');\n  }\n}\n\nfunction createLookup() {\n  return (\n    hostname: string,\n    options: LookupOptions,\n    callback: (error: NodeJS.ErrnoException | null, address: string | LookupAddress[], family?: number) => void,\n  ) => {\n    dnsLookup(hostname, options, (error, address, family) => {\n      if (error) {\n        callback(error, address, family);\n        return;\n      }\n\n      const resolvedAddresses = Array.isArray(address) ? address.map(result => result.address) : [address];\n      const blockedAddress = resolvedAddresses.find(isBlockedIp);\n","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/tools/builtin/web-fetch.ts#L114-L150","documentation":"The built-in web-fetch tool enforces an SSRF guard: before fetching, assertAllowedUrl normalizes the hostname and rejects hostnames that are blocked (localhost/internal names) or resolve to private/reserved IP ranges (loopback, link-local, RFC1918, metadata endpoints, etc.). If the target URL's hostname matches a blocked name or IP, the tool throws a WebFetchError instead of performing the request, protecting internal networks from agent-driven requests.","triggerScenarios":"Calling the webFetch tool (or its request path) with a URL whose hostname is localhost, 127.0.0.1, ::1, 169.254.169.254, an RFC1918 address (10.x, 192.168.x, 172.16-31.x), or a DNS name that resolves to any of those; also triggered when a hostname resolves to a private IP at DNS-lookup time via the createLookup hook.","commonSituations":"Pointing the tool at a local dev server (http://localhost:3000) during development; fetching cloud metadata endpoints; testing against internal/staging services behind private DNS; on-prem deployments where legitimate targets are on private networks.","solutions":["Fetch a publicly routable URL instead — the guard is intentional SSRF protection and should not be bypassed lightly.","If local/internal fetching is a legitimate requirement, reconfigure or extend the tool's allowlist options (if the tool exposes allowed-hosts configuration) rather than editing the guard.","Use a publicly reachable hostname/tunnel (e.g. a tunneling service) for the dev server so the DNS resolution is public.","If this blocks a valid public site, check DNS: the hostname may resolve to a private IP (misconfigured DNS or /etc/hosts entry)."],"exampleFix":"// before\nawait webFetchTool.execute({ context: { url: 'http://localhost:3000/api/data' } });\n\n// after (dev: expose via a tunnel or use the public URL)\nawait webFetchTool.execute({ context: { url: 'https://dev.example.com/api/data' } });","handlingStrategy":"validation","validationCode":"// Pre-check a URL against the same class of rules before calling webFetch\nfunction isPublicHttpUrl(raw: string): boolean {\n  try {\n    const u = new URL(raw);\n    if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;\n    const host = u.hostname.toLowerCase();\n    if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.internal') || host.endsWith('.local')) return false;\n    if (/^(127\\.|10\\.|192\\.168\\.|169\\.254\\.|0\\.)/.test(host)) return false;\n    if (/^::1$|^fc00:|^fe80:|^fd/i.test(host)) return false;\n    const m172 = host.match(/^172\\.(\\d+)\\./);\n    if (m172 && +m172[1] >= 16 && +m172[1] <= 31) return false;\n    return true;\n  } catch { return false; }\n}","typeGuard":"function isPublicHostname(hostname: string): boolean {\n  return !/^(localhost|127\\.|10\\.|192\\.168\\.|169\\.254\\.)/.test(hostname) && !/^172\\.(1[6-9]|2\\d|3[01])\\./.test(hostname);\n}","tryCatchPattern":"try {\n  await webFetchTool.execute({ context: { url } });\n} catch (err) {\n  if (err instanceof Error && err.message.includes('private or reserved address')) {\n    console.warn(`blocked non-public URL: ${url}`);\n  } else throw err;\n}","preventionTips":["Only feed publicly routable http(s) URLs to the web-fetch tool.","For local dev, use a tunnel with a public hostname.","Check /etc/hosts and DNS for entries mapping public names to private IPs.","Never try to bypass the SSRF guard; extend the tool's allowlist config instead if you legitimately need internal access."],"tags":["security","ssrf","web-fetch","validation"],"backgroundTag":"ssrf-blocked-url","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}