ComposioHQ/composio · error · ComposioBlockedInternalUrlError
Refusing to fetch "${host}" — it resolves to a private, loop
Error message
Refusing to fetch "${host}" — it resolves to a private, loopback, or link-local address What it means
The core SSRF protection: after resolving the URL's hostname, every resolved IP is checked against private/loopback/link-local ranges (and similar). If any address is in a blocked range, the fetch is refused before connecting, with the offending resolvedIp attached.
Source
Thrown at ts/packages/core/src/utils/ssrfGuard.node.ts:201
);
}
const host = url.hostname.replace(/^\[|\]$/g, '');
let resolved: Array<{ address: string }>;
try {
resolved = await lookup(host, { all: true, verbatim: true });
} catch {
throw new ComposioBlockedInternalUrlError(`Could not resolve host "${host}"`, { url: rawUrl });
}
if (resolved.length === 0) {
throw new ComposioBlockedInternalUrlError(`Could not resolve host "${host}"`, { url: rawUrl });
}
for (const { address } of resolved) {
if (isBlockedIp(address)) {
throw new ComposioBlockedInternalUrlError(
`Refusing to fetch "${host}" — it resolves to a private, loopback, or link-local address`,
{ url: rawUrl, resolvedIp: address }
);
}
}
// Every answer was validated, so all of them are safe to connect to, and
// resolver order is the system's own address preference.
return resolved.map(({ address }) => address);
};
/**
* Drop-in replacement for `fetch` that blocks SSRF. Validates the target, then
* connects to the address it validated, and re-validates and re-pins every
* redirect hop (redirects are followed manually up to {@link MAX_REDIRECTS}).
* Intermediate redirect bodies are cancelled; non-redirect responses are
* returned unchanged.
*View on GitHub (pinned to 64b1b85502)
Solutions
- Host the file at a genuinely public URL and pass that
- If the file is local to your app, pass the bytes/File directly instead of a URL
- For split-horizon DNS, use the public resolution path (external DNS) or an externally reachable host
Example fix
// before
await upload.uploadFileAtUrl('http://localhost:3000/tmp/report.pdf');
// after
const res = await fetch('http://localhost:3000/tmp/report.pdf');
const file = await res.blob();
await upload.uploadFile(file); Defensive patterns
Strategy: validation
Validate before calling
import { isIP } from 'node:net';
import { lookup } from 'node:dns/promises';
const addrs = await lookup(host, { all: true }).catch(() => []);
const isPrivate = (ip: string) =>
ip.startsWith('127.') || ip.startsWith('10.') || ip.startsWith('192.168.') ||
/^172\.(1[6-9]|2\d|3[01])\./.test(ip) || ip === '::1' || ip.startsWith('fe80:') || ip.startsWith('fc');
if (addrs.some(a => isPrivate(a.address))) throw new Error('URL targets private IP'); Type guard
null
Try / catch
try {
await upload.uploadFileAtUrl(url);
} catch (e) {
if (e instanceof ComposioBlockedInternalUrlError && 'resolvedIp' in (e as any)) {
// host resolves internally; upload bytes directly instead
}
} Prevention
- Never pass localhost/internal URLs to URL-upload APIs; upload bytes instead
- Prefer uploading File/ArrayBuffer for anything sourced internally
- Beware wildcard-DNS services and split-horizon DNS when choosing URLs
When it happens
Trigger: Passing a URL whose hostname resolves to 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.x, ::1, fc00::/7, etc. — including public-looking hostnames (nip.io, sslip.io, localtunnel) that map to internal IPs, and DNS rebinding attempts.
Common situations: Trying to upload from localhost ('http://localhost:3000/file') or an internal service; using wildcard-DNS services that resolve to private IPs; the SDK server running in a network where the public host resolves internally via split-horizon DNS.
Related errors
- Refusing to fetch a malformed or non-http(s) URL
- Refusing to fetch "{parsed.hostname}" because it resolves to
- Refusing to fetch a malformed URL
- Refusing to fetch a non-http(s) URL (scheme "${url.protocol}
- Could not resolve host "${host}"
AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28).
Data as JSON: /api/errors/3d9c4508bac100a1.
Report an issue: GitHub.