Vulnerability DatabaseGHSA-qm7x-j9xp-x3gv

GHSA-qm7x-j9xp-x3gv: 
JavaScript vulnerability analysis and mitigation

Summary

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.

Security Policy & Trust Boundary

Assessment DimensionRepository-Specific Boundary & Policy Alignment
Target Component & Symbolsrc/utils/ssrf-protection.ts (SSRFProtection.validateUrlSync) in czlonkowski/n8n-mcp (verified on live HEAD fc6a6e9e2cc6)
Vulnerability Class & VectorSSRF Protection Bypass in validateUrlSync and validateWebhookUrl via Trailing-Dot FQDNs and RFC 6761 *.localhost Subdomains (CWE-918, CWE-184)
Trust Boundary CrossedUntrusted 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 ScopePrivately 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.

Root Cause Analysis

  1. Trailing-Dot FQDN Bypass of 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').
    • Neither validateWebhookUrl() (line 349) nor validateUrlSync() (line 645) strips trailing root dots (.replace(/\.+$/, '')) before calling isCloudMetadata(hostname) or isLoopbackHost(hostname).
    • Consequently:
      • 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).
      • In validateWebhookUrl(), http://metadata.google.internal./ bypasses Step 2 (isCloudMetadata(hostname)), permitting GCP metadata access in permissive mode.
  2. RFC 6761 *.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').
    • Under RFC 6761 (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.

Impact

  • Bypass of CLOUD_METADATA and LOCALHOST_PATTERNS hostname blocks in validateUrlSync (across strict, moderate, and permissive modes) and isCloudMetadata in validateWebhookUrl.

Benign Verification

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);

Remediation

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' };

Reporter Contact

Reported by cc.simply.spolit@gmail.com.


Source: NVD

Related JavaScript vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

GHSA-j5fr-jm8f-m549HIGH7.7
  • JavaScript logoJavaScript
  • hubot-grafana
NoYesOct 29, 2026
GHSA-w658-mh9p-ww8fMEDIUM6.5
  • JavaScript logoJavaScript
  • @backstage/plugin-catalog-backend
NoYesOct 28, 2026
GHSA-q7jr-6fm4-vvhgMEDIUM4.3
  • JavaScript logoJavaScript
  • n8n-mcp
NoYesOct 29, 2026
GHSA-qm7x-j9xp-x3gvLOW3.1
  • JavaScript logoJavaScript
  • n8n-mcp
NoYesOct 29, 2026
GHSA-6v8j-6w28-2w8rMEDIUMN/A
  • JavaScript logoJavaScript
  • pi-subagents
NoYesOct 29, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Additional Wiz resources

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management