GHSA-wch5-xp77-fxg4 Flowise: Cross-Workspace OAuth2 Credential Metadata Leak
SummaryThree 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 CauseVulnerable code: no workspace scopingAll three OAuth2 handlers query the `Credential` table by `id` only:**`packages/server/src/routes/oauth2/index.ts:80-82`** (authorize)**`packages/server/src/routes/oauth2/index.ts:183-185`** (callback)**`packages/server/src/routes/oauth2/index.ts:314-316`** (refresh)Correct pattern (same codebase)The standard credential service correctly enforces workspace isolation:**`packages/server/src/services/credentials/index.ts:130-132`**Authentication bypass via whitelist**`packages/server/src/utils/constants.ts:40-41`****`packages/server/src/index.ts:223-225`** — prefix-matched whitelist skips all auth:Attack ScenariosScenario A: Cross-Workspace Credential Metadata LeakAn 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`.**Response:**Scenario B: Unauthenticated Token Injection via Forged CallbackThe 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.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):Scenario C: Unauthenticated Token RefreshAn 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.**Response:**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 ConceptPrerequisitesA 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**Expected result if credential exists and has a refresh token:****Expected result if credential not found:**Step 2 — Cross-workspace authorize (requires any valid session)**Expected result — victim credential's OAuth2 config is leaked:**Step 3 — Forge callback to inject attacker-controlled tokensThe 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 scriptImpact| Vector | Auth Required | Impact |
|--------|--------------|--------|
| Credential metadata leak via `/authorize` | Low (any session) | Exposes `client_id`, `scope`, `redirect_uri` from any workspace's credential |
| Token injection via `/callback` | None | Overwrite any credential's stored OAuth2 tokens with attacker-controlled values |
| Token theft via `/refresh` | None | Obtain a fresh `access_token` for any credential's connected service (Microsoft 365, Google, etc.) |**Chained impact:** An attacker who obtains a single credential UUID (via IDOR, log exposure, or brute-force of UUIDs) can silently refresh and steal OAuth2 access tokens for external services like Microsoft Graph, Google Workspace, or any custom OAuth2 provider — without any authentication to the Flowise instance.Affected Components| File | Lines | Issue |
|------|-------|-------|
| `packages/server/src/routes/oauth2/index.ts` | 80-82 | `findOneBy({ id })` — no `workspaceId` |
| `packages/server/src/routes/oauth2/index.ts` | 183-185 | `findOneBy({ id: state })` — no `workspaceId` |
| `packages/server/src/routes/oauth2/index.ts` | 314-316 | `findOneBy({ id })` — no `workspaceId` |
| `packages/server/src/utils/constants.ts` | 40 | `/callback` whitelisted from auth |
| `packages/server/src/utils/constants.ts` | 41 | `/refresh` whitelisted from auth |Remediation**Add `workspaceId` to all credential lookups** in the OAuth2 routes, matching the pattern already used in `services/credentials/index.ts:130-132`:**Remove `/callback` and `/refresh` from `WHITELIST_URLS`** or implement a signed, time-limited state token that authenticates the callback without a session.**Replace the `state` parameter** with a cryptographically random nonce bound to the user's session (see also Finding 8).**Do not return `access_token` in the `/refresh` response body.** The token should only be stored server-side in the encrypted credential data, never sent to the caller.
CVSS v4.0 score: 7.6