{"record":{"id":"e8cd9daa22e55cfd","repo":"different-ai/openwork","slug":"attachment-hostname-hostname-resolved-to-a-priv","errorCode":null,"errorMessage":"Attachment hostname ${hostname} resolved to a private or reserved address (${privateAddress.address}).","messagePattern":"Attachment hostname (.+?) resolved to a private or reserved address \\((.+?)\\)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/server/src/extensions/google-workspace.ts","lineNumber":844,"sourceCode":"  if (isIP(hostname) !== 0) return;\n  let addresses: LookupAddress[];\n  try {\n    addresses = await lookup(hostname, { all: true, verbatim: true });\n  } catch {\n    throw new ApiError(502, \"attachment_fetch_failed\", \"Attachment url hostname could not be resolved\", { hostname });\n  }\n  if (!addresses.length || addresses.some((entry) => isLocalManagedMcpPrivateAddress(entry.address))) {\n    throw new ApiError(400, \"invalid_payload\", \"Attachment url must resolve to a public address\", { hostname });\n  }\n}\n\nconst resolveGmailAttachmentAddresses: GmailAttachmentAddressResolver = (hostname, options) => lookup(hostname, options);\n\nfunction validateGmailAttachmentAddresses(hostname: string, addresses: LookupAddress[]): void {\n  if (!addresses.length) throw new Error(`Attachment hostname ${hostname} did not resolve.`);\n  const privateAddress = addresses.find((entry) => isLocalManagedMcpPrivateAddress(entry.address));\n  if (privateAddress) {\n    throw new Error(`Attachment hostname ${hostname} resolved to a private or reserved address (${privateAddress.address}).`);\n  }\n}\n\n/**\n * Resolves and validates the attachment host inside the socket connector. The\n * same answers are handed to net.connect, closing the DNS-rebinding window\n * between a preflight lookup and the actual connection.\n */\nexport function createGmailAttachmentPublicLookup(\n  resolver: GmailAttachmentAddressResolver = resolveGmailAttachmentAddresses,\n): LookupFunction {\n  return (hostname, options, callback) => {\n    const lookupOptions: LookupAllOptions = { ...options, all: true, verbatim: true };\n    void resolver(hostname, lookupOptions).then((addresses) => {\n      try {\n        validateGmailAttachmentAddresses(hostname, addresses);\n      } catch (error) {\n        callback(error instanceof Error ? error : new Error(\"Attachment hostname lookup failed.\"), []);","sourceCodeStart":826,"sourceCodeEnd":862,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/extensions/google-workspace.ts#L826-L862","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n; attachments.example.com A 127.0.0.1, A 203.0.113.10 (mixed)\n// after\n; attachments.example.com A 203.0.113.10 (public only)","handlingStrategy":"try-catch","validationCode":"import { lookup } from \"node:dns/promises\";\nasync function allAddressesPublic(hostname: string): Promise<boolean> {\n  const addrs = await lookup(hostname, { all: true, verbatim: true });\n  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\"));\n}","typeGuard":null,"tryCatchPattern":"try {\n  await fetchGmailAttachment({ url });\n} catch (e) {\n  if (e instanceof Error && /private or reserved address/.test(e.message)) {\n    // Do not bypass: hostname's DNS includes a private IP. Fix records or use another host.\n    throw new Error(`Refusing ${url}: DNS returned a private address`);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["ssrf","dns-rebinding","security","gmail"],"backgroundTag":"ssrf-private-address-blocked","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}