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

  1. Remove private/reserved A/AAAA records from the hostname's DNS.
  2. Point the URL at a hostname that resolves exclusively to public addresses.
  3. If this appears during a security test, treat it as the SSRF guard working — do not bypass it; route through a public proxy instead.
  4. 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

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


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/e8cd9daa22e55cfd. Report an issue: GitHub.