n8n-io/n8n · error · Error
Webhook URL cannot target private/internal IP addresses
Error message
Webhook URL cannot target private/internal IP addresses
What it means
Thrown from `validateWebhookUrl` when the hostname itself is a literal private/loopback/link-local IPv4 or IPv6 address matched by `isPrivateIp` (e.g. `10.x`, `172.16-31.x`, `192.168.x`, `169.254.x`, `0.0.0.0`, `fc/fd`, `fe80:`, `::1`). This is the literal-IP branch of SSRF protection — it blocks direct targeting of internal address space before any DNS lookup. The hostname has already passed the localhost/private-name checks above.
Source
Thrown at packages/@n8n/ai-workflow-builder.ee/evaluations/cli/webhook.ts:130
const url = new URL(webhookUrl);
if (url.protocol !== 'https:') {
throw new Error(`Webhook URL must use HTTPS. Got: ${url.protocol}`);
}
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);View on GitHub (pinned to 5ac6606e81)
Solutions
- Use the public hostname of the service rather than an RFC1918 IP literal.
- Route through a public ingress/proxy that itself reaches the internal host.
- If delivery to an internal host is genuinely required, send the webhook from outside this SSRF-restricted code path (e.g. your own service consuming eval output from stdout/a file).
Example fix
// before
validateWebhookUrl('https://10.0.0.5/hook');
// after
validateWebhookUrl('https://ingress.example.com/hook'); Defensive patterns
Strategy: validation
Validate before calling
// mirror the isPrivateIp predicate at the call site
function isPrivateIp(ip: string): boolean {
return [/^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./, /^169\.254\./, /^0\.0\.0\.0$/].some((p) => p.test(ip))
|| /^::1$/.test(ip) || ip.startsWith('fe80:') || ip.startsWith('fc') || ip.startsWith('fd');
}
const h = new URL(webhookUrl).hostname.toLowerCase();
if (isPrivateIp(h)) throw new Error(`webhook host ${h} is a private IP literal`); Prevention
- Never paste resolved IP literals into webhook config — use hostnames.
- Keep a shared `isPrivateIp` helper and reuse it for all egress-URL validations so checks stay in sync.
- In review, flag any new config that points a webhook at an RFC1918 address.
When it happens
Trigger: `--webhook-url https://10.0.0.5/hook`, `https://192.168.1.10/hook`, `https://172.20.0.3/hook`, or IPv6 unique-local forms. URL is HTTPS, hostname is not `localhost`, but matches a private CIDR pattern.
Common situations: Targeting an internal service mesh address, a Docker bridge IP, or a corp-network host by IP. Sometimes the result of resolving a hostname ahead of time and pasting the IP into config.
Related errors
- Webhook URL must use HTTPS. Got: ${url.protocol}
- Webhook URL cannot target localhost
- Webhook URL cannot target internal hostname: ${hostname}
- 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/6cd02db42f5545ed.
Report an issue: GitHub.