different-ai/openwork · error · Error
Attachment hostname ${hostname} resolved to a private or res
Error message
Attachment hostname ${hostname} resolved to a private or reserved address (${privateAddress.address}). What it means
The same connector-side validator also rejects any resolved address that is private or reserved, throwing a plain Error naming the offending address. Because the validated answers are the same ones handed to net.connect, this prevents DNS-rebinding from sneaking a connection to an internal address even when the hostname itself looked public.
Source
Thrown at apps/server/src/extensions/google-workspace.ts:844
if (isIP(hostname) !== 0) return;
let addresses: LookupAddress[];
try {
addresses = await lookup(hostname, { all: true, verbatim: true });
} catch {
throw new ApiError(502, "attachment_fetch_failed", "Attachment url hostname could not be resolved", { hostname });
}
if (!addresses.length || addresses.some((entry) => isLocalManagedMcpPrivateAddress(entry.address))) {
throw new ApiError(400, "invalid_payload", "Attachment url must resolve to a public address", { hostname });
}
}
const resolveGmailAttachmentAddresses: GmailAttachmentAddressResolver = (hostname, options) => lookup(hostname, options);
function validateGmailAttachmentAddresses(hostname: string, addresses: LookupAddress[]): void {
if (!addresses.length) throw new Error(`Attachment hostname ${hostname} did not resolve.`);
const privateAddress = addresses.find((entry) => isLocalManagedMcpPrivateAddress(entry.address));
if (privateAddress) {
throw new Error(`Attachment hostname ${hostname} resolved to a private or reserved address (${privateAddress.address}).`);
}
}
/**
* Resolves and validates the attachment host inside the socket connector. The
* same answers are handed to net.connect, closing the DNS-rebinding window
* between a preflight lookup and the actual connection.
*/
export function createGmailAttachmentPublicLookup(
resolver: GmailAttachmentAddressResolver = resolveGmailAttachmentAddresses,
): LookupFunction {
return (hostname, options, callback) => {
const lookupOptions: LookupAllOptions = { ...options, all: true, verbatim: true };
void resolver(hostname, lookupOptions).then((addresses) => {
try {
validateGmailAttachmentAddresses(hostname, addresses);
} catch (error) {
callback(error instanceof Error ? error : new Error("Attachment hostname lookup failed."), []);View on GitHub (pinned to 2b7df46e8a)
Solutions
- Remove private/reserved A/AAAA records from the hostname's DNS.
- Point the URL at a hostname that resolves exclusively to public addresses.
- If this appears during a security test, treat it as the SSRF guard working — do not bypass it; route through a public proxy instead.
- For legitimate internal delivery, fetch the attachment out-of-band and provide it by local path inside a workspace root.
Example fix
// before ; attachments.example.com A 127.0.0.1, A 203.0.113.10 (mixed) // after ; attachments.example.com A 203.0.113.10 (public only)
Defensive patterns
Strategy: try-catch
Validate before calling
import { lookup } from "node:dns/promises";
async function allAddressesPublic(hostname: string): Promise<boolean> {
const addrs = await lookup(hostname, { all: true, verbatim: true });
return addrs.every((a) => !(a.address.startsWith("10.") || a.address.startsWith("192.168.") || a.address.startsWith("169.254.") || a.address === "127.0.0.1" || a.address === "::1"));
} Try / catch
try {
await fetchGmailAttachment({ url });
} catch (e) {
if (e instanceof Error && /private or reserved address/.test(e.message)) {
// Do not bypass: hostname's DNS includes a private IP. Fix records or use another host.
throw new Error(`Refusing ${url}: DNS returned a private address`);
}
throw e;
} Prevention
- Keep hostnames' DNS records free of private/reserved addresses.
- Watch for DNS-rebinding on URLs sourced from untrusted emails — this error is the guard firing.
- Audit multi-record hostnames for mixed public/private IPs.
- Use dedicated public-only hostnames for attachment delivery.
When it happens
Trigger: Between the initial public-resolution check and the socket connect, DNS re-resolves (or a rebinding/retry answers) with a private/reserved IP such as 127.0.0.1, 10.x, 192.168.x, 169.254.x, or ::1.
Common situations: DNS rebinding attacks on attacker-supplied attachment URLs; load balancers that return internal health-check addresses; split-horizon DNS where a later query hits the internal view; misconfigured multi-record hostnames mixing public and private IPs.
Related errors
- the hostname resolves to a private or reserved address (${ad
- the address is private or reserved
- MCP_URL_BLOCKED
- External URL protocol "${parsed.protocol}" is not allowed.
- invalid_payload
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/e8cd9daa22e55cfd.
Report an issue: GitHub.