
Cloud Vulnerability DB
A community-led vulnerabilities database
Audited Repository: czlonkowski/n8n-mcp (verified at commit fc6a6e9e2cc6).
In src/utils/ssrf-protection.ts (<= v2.99.1), SSRFProtection.validateUrlSync() and SSRFProtection.validateWebhookUrl() normalize url.hostname via .toLowerCase() and IPv6 bracket-stripping, without stripping trailing DNS root-zone dots (.) and without checking RFC 6761 .localhost subdomains in isLoopbackHost().
Because the WHATWG URL parser preserves trailing dots in url.hostname (e.g., new URL('<http://metadata.google.internal./computeMetadata/v1/'>).hostname === 'metadata.google.internal.' and new URL('<http://localhost.:5678/'>).hostname === 'localhost.'), exact-match checks against CLOUD_METADATA and LOCALHOST_PATTERNS are bypassed.
| Assessment Dimension | Repository-Specific Boundary & Policy Alignment |
|---|---|
| Target Component & Symbol | src/utils/ssrf-protection.ts (SSRFProtection.validateUrlSync) in czlonkowski/n8n-mcp (verified on live HEAD fc6a6e9e2cc6) |
| Vulnerability Class & Vector | SSRF Protection Bypass in validateUrlSync and validateWebhookUrl via Trailing-Dot FQDNs and RFC 6761 *.localhost Subdomains (CWE-918, CWE-184) |
| Trust Boundary Crossed | Untrusted URL, redirect, or external resource targets processed by SSRFProtection.validateUrlSync in src/utils/ssrf-protection.ts cross czlonkowski/n8n-mcp's outbound network egress guard into internal/loopback networks or credentialed endpoints (CWE-918, CWE-184) |
| Disclosure & Policy Scope | Privately reported under czlonkowski/n8n-mcp's coordinated vulnerability disclosure policy (SECURITY.md, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
In czlonkowski/n8n-mcp (scope <= 2.99.1, commit fc6a6e9e2cc6), SSRFProtection.validateUrlSync, SSRFProtection.validateWebhookUrl in src/utils/ssrf-protection.ts enforces outbound HTTP/network target restrictions when fetching external URLs or handling OAuth/webhook redirects. Allowing unvalidated schemes, redirect hops, or private/metadata IP ranges past SSRFProtection.validateUrlSync (CWE-918, CWE-184) enables untrusted callers or remote servers to pivot across czlonkowski/n8n-mcp's network perimeter into internal services or leak bearer credentials. |
CLOUD_METADATA and LOCALHOST_PATTERNS (src/utils/ssrf-protection.ts:129-139, 349-359, 645-652):CLOUD_METADATA (lines 58-74) and LOCALHOST_PATTERNS (lines 80-89) are Set<string> collections of bare hostnames ('metadata.google.internal', 'metadata', 'instance-data', 'localhost', 'localhost.localdomain').validateWebhookUrl() (line 349) nor validateUrlSync() (line 645) strips trailing root dots (.replace(/\.+$/, '')) before calling isCloudMetadata(hostname) or isLoopbackHost(hostname).SSRFProtection.validateUrlSync('<http://metadata.google.internal./computeMetadata/v1/'>), SSRFProtection.validateUrlSync('<http://localhost.:5678/'>), and SSRFProtection.validateUrlSync('<http://localhost.localdomain.:5678/'>) return { valid: true } even in strict mode (incomplete remediation of GHSA-4ggg-h7ph-26qr / CVE-2026-39974 and GHSA-56c3-vfp2-5qqj / CVE-2026-42449 for downstream consumers using validateUrlSync).validateWebhookUrl(), http://metadata.google.internal./ bypasses Step 2 (isCloudMetadata(hostname)), permitting GCP metadata access in permissive mode.*.localhost Subdomain Bypass in isLoopbackHost (src/utils/ssrf-protection.ts:137-139):isLoopbackHost(host) only checks LOCALHOST_PATTERNS.has(host) ('localhost', 'localhost.localdomain'), omitting host.endsWith('.localhost').systemd-resolved, nss-myhostname, macOS, Windows, and Docker), all *.localhost names (e.g., http://admin.localhost:5678/) resolve locally to 127.0.0.1 / ::1, yet validateUrlSync returns { valid: true } in strict mode.CLOUD_METADATA and LOCALHOST_PATTERNS hostname blocks in validateUrlSync (across strict, moderate, and permissive modes) and isCloudMetadata in validateWebhookUrl.import assert from "node:assert/strict";
function isLoopbackOrBlockedHost(rawHostname: string): boolean {
const host = rawHostname.toLowerCase().replace(/\.+$/, "");
return (
host === "localhost" ||
host.endsWith(".localhost") ||
host === "localhost.localdomain" ||
host.endsWith(".localhost.localdomain") ||
host === "metadata.google.internal"
);
}
// Trailing FQDN dot and subdomain of .localhost must both be blocked:
assert.equal(isLoopbackOrBlockedHost("localhost."), true);
assert.equal(isLoopbackOrBlockedHost("metadata.google.internal."), true);
assert.equal(isLoopbackOrBlockedHost("admin.localhost"), true);
assert.equal(isLoopbackOrBlockedHost("example.com"), false);Normalize hostnames by stripping trailing dots in both validateWebhookUrl and validateUrlSync, and treat *.localhost and *.localhost.localdomain as loopback hosts in isLoopbackHost:
--- a/src/utils/ssrf-protection.ts
+++ b/src/utils/ssrf-protection.ts
@@ -135,7 +135,12 @@
* PRIVATE_IP_RANGES).
*/
private static isLoopbackHost(host: string): boolean {
- return LOCALHOST_PATTERNS.has(host) || (isIPv4(host) && host.startsWith('127.'));
+ return (
+ LOCALHOST_PATTERNS.has(host) ||
+ host.endsWith('.localhost') ||
+ host.endsWith('.localhost.localdomain') ||
+ (isIPv4(host) && host.startsWith('127.'))
+ );
}
/**
@@ -345,11 +345,14 @@
return { valid: false, reason: 'Invalid protocol. Only HTTP/HTTPS allowed.' };
}
- // Get hostname and strip IPv6 brackets if present
+ // Get hostname, strip IPv6 brackets and trailing FQDN dots
let hostname = url.hostname.toLowerCase();
- // Remove IPv6 brackets for consistent comparison
if (hostname.startsWith('[') && hostname.endsWith(']')) {
hostname = hostname.slice(1, -1);
+ }
+ hostname = hostname.replace(/\.+$/, '');
+ if (!hostname) {
+ return { valid: false, reason: 'Invalid URL format' };
}
// Step 2: ALWAYS block cloud metadata endpoints (all modes)
@@ -646,6 +646,10 @@
if (hostname.startsWith('[') && hostname.endsWith(']')) {
hostname = hostname.slice(1, -1);
}
+ hostname = hostname.replace(/\.+$/, '');
+ if (!hostname) {
+ return { valid: false, reason: 'Invalid URL format' };
+ }
if (SSRFProtection.isCloudMetadata(hostname)) {
return { valid: false, reason: 'Cloud metadata endpoint blocked' };Reported by cc.simply.spolit@gmail.com.
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."