n8n-io/n8n · error · Error

Webhook URL cannot target internal hostname: ${hostname}

Error message

Webhook URL cannot target internal hostname: ${hostname}

What it means

`validateWebhookUrl` rejects hostnames that equal or end with `.internal`, `.intranet`, `.corp`, `.private`, or `.local`. These are heuristic internal-only DNS suffixes that bypass the literal-IP check; blocking them closes the SSRF gap for corp DNS names. The match is case-insensitive (hostname was lowercased) and covers both the bare suffix and any subdomain of it.

Source

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

	const hostname = url.hostname.toLowerCase();

	if (
		hostname === 'localhost' ||
		hostname === '127.0.0.1' ||
		hostname === '::1' ||
		hostname === '[::1]'
	) {
		throw new Error('Webhook URL cannot target localhost');
	}

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

	const blockedHostnames = ['internal', 'intranet', 'corp', 'private', 'local'];
	for (const blocked of blockedHostnames) {
		if (hostname === blocked || hostname.endsWith(`.${blocked}`)) {
			throw new Error(`Webhook URL cannot target internal hostname: ${hostname}`);
		}
	}
}

/**
 * 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');
	}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Use the public/resolvable hostname of the receiver.
  2. Add a public DNS record (or tunnel) for the service and reference that instead.
  3. Pick a hostname that does not terminate in one of the blocked suffixes — e.g. `evalsink.dev.company.com`.

Example fix

// before
validateWebhookUrl('https://eval-sink.corp/hook');
// after
validateWebhookUrl('https://eval-sink.company.com/hook');
Defensive patterns

Strategy: validation

Validate before calling

const BLOCKED_SUFFIXES = ['internal', 'intranet', 'corp', 'private', 'local'];
function isBlockedSuffix(hostname: string): boolean {
  return BLOCKED_SUFFIXES.some((s) => hostname === s || hostname.endsWith('.' + s));
}
const h = new URL(webhookUrl).hostname.toLowerCase();
if (isBlockedSuffix(h)) throw new Error(`webhook host ${h} uses a blocked internal suffix`);

Prevention

When it happens

Trigger: `--webhook-url https://eval-sink.internal/hook`, `https://api.corp.company.internal/hook`, `https://receiver.local`, `https://thing.private`. URL is HTTPS, hostname is not a loopback/private literal, but matches a blocked suffix.

Common situations: Company-internal DNS names pasted into shared config, or a dev `.local` mDNS hostname used during testing. The `.local` case often catches macOS Bonjour names.

Related errors


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