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
- Use the public/resolvable hostname of the receiver.
- Add a public DNS record (or tunnel) for the service and reference that instead.
- 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
- Use a public DNS zone (e.g. `.company.com`) for webhook receivers, not internal suffixes.
- Keep the blocked-suffix list in a shared module so the call-site and library checks do not diverge.
- Document the blocked suffixes alongside the webhook-flag help text.
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
- Webhook URL must use HTTPS. Got: ${url.protocol}
- Webhook URL cannot target localhost
- Webhook URL cannot target private/internal IP addresses
- Webhook URL hostname resolves to a private/internal IP addre
- Invalid skill at ${sourceDirectory}: ${errors.join('; ')}
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/9bcf3cb027cf749c.
Report an issue: GitHub.