{"record":{"id":"6cd02db42f5545ed","repo":"n8n-io/n8n","slug":"webhook-url-cannot-target-private-internal-ip-addr","errorCode":null,"errorMessage":"Webhook URL cannot target private/internal IP addresses","messagePattern":"Webhook URL cannot target private/internal IP addresses","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@n8n/ai-workflow-builder.ee/evaluations/cli/webhook.ts","lineNumber":130,"sourceCode":"\tconst url = new URL(webhookUrl);\n\n\tif (url.protocol !== 'https:') {\n\t\tthrow new Error(`Webhook URL must use HTTPS. Got: ${url.protocol}`);\n\t}\n\n\tconst hostname = url.hostname.toLowerCase();\n\n\tif (\n\t\thostname === 'localhost' ||\n\t\thostname === '127.0.0.1' ||\n\t\thostname === '::1' ||\n\t\thostname === '[::1]'\n\t) {\n\t\tthrow new Error('Webhook URL cannot target localhost');\n\t}\n\n\tif (isPrivateIp(hostname)) {\n\t\tthrow new Error('Webhook URL cannot target private/internal IP addresses');\n\t}\n\n\tconst blockedHostnames = ['internal', 'intranet', 'corp', 'private', 'local'];\n\tfor (const blocked of blockedHostnames) {\n\t\tif (hostname === blocked || hostname.endsWith(`.${blocked}`)) {\n\t\t\tthrow new Error(`Webhook URL cannot target internal hostname: ${hostname}`);\n\t\t}\n\t}\n}\n\n/**\n * Validate webhook URL with DNS resolution for comprehensive SSRF protection.\n * Resolves the hostname and validates that resolved IPs are not private/internal.\n */\nexport async function validateWebhookUrlWithDns(webhookUrl: string): Promise<void> {\n\tvalidateWebhookUrl(webhookUrl);\n\n\tconst url = new URL(webhookUrl);","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/ai-workflow-builder.ee/evaluations/cli/webhook.ts#L112-L148","documentation":"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.","triggerScenarios":"`--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.","commonSituations":"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.","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)."],"exampleFix":"// before\nvalidateWebhookUrl('https://10.0.0.5/hook');\n// after\nvalidateWebhookUrl('https://ingress.example.com/hook');","handlingStrategy":"validation","validationCode":"// mirror the isPrivateIp predicate at the call site\nfunction isPrivateIp(ip: string): boolean {\n  return [/^127\\./, /^10\\./, /^172\\.(1[6-9]|2\\d|3[01])\\./, /^192\\.168\\./, /^169\\.254\\./, /^0\\.0\\.0\\.0$/].some((p) => p.test(ip))\n    || /^::1$/.test(ip) || ip.startsWith('fe80:') || ip.startsWith('fc') || ip.startsWith('fd');\n}\nconst h = new URL(webhookUrl).hostname.toLowerCase();\nif (isPrivateIp(h)) throw new Error(`webhook host ${h} is a private IP literal`);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["webhook","ssrf","security","validation","private-ip"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}