n8n-io/n8n · error · Error

Webhook URL hostname resolves to a private/internal IP addre

Error message

Webhook URL hostname resolves to a private/internal IP address

What it means

`validateWebhookUrlWithDns` resolves the hostname's A records via `dns.resolve` and rejects the URL if any returned IPv4 is private/internal per `isPrivateIp`. This is the DNS-rebinding defense: a hostname that looks public can still resolve to an internal IP, so the literal-hostname checks alone are insufficient. The first private record found trips the throw.

Source

Thrown at packages/@n8n/ai-workflow-builder.ee/evaluations/cli/webhook.ts:159

/**
 * Validate webhook URL with DNS resolution for comprehensive SSRF protection.
 * Resolves the hostname and validates that resolved IPs are not private/internal.
 */
export async function validateWebhookUrlWithDns(webhookUrl: string): Promise<void> {
	validateWebhookUrl(webhookUrl);

	const url = new URL(webhookUrl);
	const hostname = url.hostname.toLowerCase();

	if (isPrivateIp(hostname)) {
		throw new Error('Webhook URL cannot target private/internal IP addresses');
	}

	try {
		const addresses = await dns.resolve(hostname);
		for (const ip of addresses) {
			if (isPrivateIp(ip)) {
				throw new Error('Webhook URL hostname resolves to a private/internal IP address');
			}
		}

		try {
			const ipv6Addresses = await dns.resolve6(hostname);
			for (const ip of ipv6Addresses) {
				if (isPrivateIp(ip)) {
					throw new Error('Webhook URL hostname resolves to a private/internal IP address');
				}
			}
		} catch {
			// IPv6 resolution may fail if no AAAA records exist, which is fine
		}
	} catch (error) {
		if (error instanceof Error && error.message.includes('private/internal')) {
			throw error;
		}
	}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Ensure the hostname's public A records resolve to public IPs only.
  2. If split-horizon DNS is the cause, run the eval from a network position that gets the public view, or pin the webhook to a host with consistent public resolution.
  3. If you genuinely need an internal target, do not use `validateWebhookUrlWithDns` — deliver results out-of-band from outside this code path.

Example fix

// before
// webhook.example.com A record -> 10.0.0.5
await validateWebhookUrlWithDns('https://webhook.example.com/hook');
// after
// fix DNS so webhook.example.com -> 203.0.113.5
await validateWebhookUrlWithDns('https://webhook.example.com/hook');
Defensive patterns

Strategy: try-catch

Validate before calling

import dns from 'node:dns/promises';

async function preflightPublicIpv4(hostname: string): Promise<void> {
  let addrs: string[];
  try { addrs = await dns.resolve(hostname); }
  catch (e) { throw new Error(`cannot resolve ${hostname}: ${(e as Error).message}`); }
  if (addrs.some(isPrivateIp)) {
    throw new Error(`${hostname} resolves to a private IPv4; webhook rejected (SSRF)`);
  }
}
await preflightPublicIpv4(new URL(webhookUrl).hostname);

Try / catch

try {
  await validateWebhookUrlWithDns(webhookUrl);
} catch (e) {
  if (e instanceof Error && e.message.includes('resolves to a private/internal IP')) {
    // SSRF guard tripped — do NOT retry; surface to operator and skip webhook
    logger.error(`webhook rejected by SSRF guard: ${e.message}`);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: A public-looking hostname whose A record returns `10.x`, `192.168.x`, `127.x`, etc. — either intentionally (internal service exposed via a public DNS name) or maliciously (DNS rebinding). Also triggered by split-horizon DNS where the eval host resolves to the internal view.

Common situations: Running evals from inside a corp network where DNS returns internal IPs for `.com` names; a test domain that points at `127.0.0.1` for fun; or a webhook provider whose DNS temporarily includes an internal address.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/b11ef5d16a60f949. Report an issue: GitHub.