n8n-io/n8n · error · Error
Webhook URL cannot target localhost
Error message
Webhook URL cannot target localhost
What it means
`validateWebhookUrl` blocks webhook URLs whose hostname is a literal loopback (`localhost`, `127.0.0.1`, `::1`, `[::1]`). This is the localhost branch of the SSRF guard — even with HTTPS, allowing loopback targets would let eval output (and the HMAC-signed body) be POSTed to services on the same host. The comparison is a case-insensitive exact string match on `url.hostname`.
Source
Thrown at packages/@n8n/ai-workflow-builder.ee/evaluations/cli/webhook.ts:126
* Note: This performs synchronous hostname string validation.
* For full SSRF protection, use validateWebhookUrlWithDns() which also resolves DNS.
*/
export function validateWebhookUrl(webhookUrl: string): void {
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.
*/View on GitHub (pinned to 5ac6606e81)
Solutions
- Point at a public/TLS-terminated receiver (e.g. an HTTPS ngrok/cloudflared tunnel in front of your local service).
- Replace the loopback hostname with the machine's resolvable public hostname behind HTTPS.
- Remove the `--webhook-url` flag entirely during local debugging if you don't actually need webhook delivery.
Example fix
// before
validateWebhookUrl('https://localhost:9000/eval');
// after
validateWebhookUrl('https://my-tunnel.example.dev/eval'); Defensive patterns
Strategy: validation
Validate before calling
const LOOPBACK = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
function isLoopbackHost(hostname: string): boolean {
return LOOPBACK.has(hostname.toLowerCase());
}
// before validateWebhookUrl:
const h = new URL(webhookUrl).hostname.toLowerCase();
if (isLoopbackHost(h)) throw new Error('webhook host is loopback; use a public host'); Prevention
- Default your dev webhook config to an HTTPS tunnel hostname rather than localhost.
- Document that loopback webhook targets are blocked by SSRF policy so operators stop trying.
- Audit shared config files for `localhost`/`127.0.0.1` webhook entries.
When it happens
Trigger: `--webhook-url https://localhost:8080/hook`, `https://127.0.0.1:8080/hook`, or the IPv6 loopback forms. The URL is HTTPS and parses, but the hostname matches a loopback literal.
Common situations: Developer points eval output at a local receiver for debugging, or a config templated from a dev environment leaks `localhost` into a shared config. IPv6 `[::1]` is the easy miss because the bracketed form looks different from `127.0.0.1`.
Related errors
- Webhook URL must use HTTPS. Got: ${url.protocol}
- Webhook URL cannot target private/internal IP addresses
- 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/2b377335803eddc2.
Report an issue: GitHub.