Summary
An authenticated FastGPT user can save a workflow node that points to another user's private HTTP toolset by using a crafted saved tool id such as:
http-<victim_toolset_app_id>/<tool_name>
The normal toolset routes correctly deny access to that toolset. For example, the attacker cannot preview it, read its detail, list its versions, update it, or obtain read/write/manage permission. The problem is that the workflow save/runtime path does not apply the same authorization check to the referenced toolset. After the crafted workflow is saved, /api/v2/chat/completions resolves the saved reference and executes the victim-owned HTTP tool.
I reproduced this on:
v4.14.17
v4.14.22
v4.14.23
v4.15.0-beta3
I am reporting this as CWE-863: Incorrect Authorization. It is not a direct SSRF report and it is not an arbitrary code execution report.
Impact
The attacker can cause FastGPT to execute a private HTTP toolset that belongs to another user/team. In the basic PoC, the attacker receives the response of a victim-owned HTTP tool. In an additional local-only impact test, the victim toolset carried a synthetic credential for a mock external SaaS endpoint. The attacker still had no official toolset permission, but the saved workflow runtime used the victim toolset's credentialed capability to read a tenant-private marker and update external mock state.
The external-service test used only a local mock service and a synthetic credential. No real third-party service, cloud metadata endpoint, intranet host, production system, or real secret was used.
Security Boundary
The crossed boundary is the ownership and authorization boundary between workflow apps and private HTTP toolsets.
In the PoC:
user_a owns a private HTTP toolset.
user_b owns a separate workflow app.
user_b has no permission on user_a's toolset.
- Official toolset routes enforce that denial.
- Workflow runtime still executes
user_a's toolset when user_b saves a crafted reference in user_b's own workflow.
This affects another user, not only the reporter's own account.
Preconditions
- The attacker is authenticated.
- The attacker can create/save/run their own workflow app.
- A victim has a private HTTP toolset.
- The attacker knows a valid combined tool id in the form
http-<toolset_app_id>/<tool_name>.
I am not claiming low attack complexity for identifier discovery yet. I tested a possible product collaboration path, but my local OSS build did not expose the formal product collaboration API, so I am keeping AC:H.
Environment Used
Main local reproduction:
- FastGPT bound to
http://127.0.0.1:3300
- local mock HTTP/OpenAPI service reachable from FastGPT containers
- two ordinary users with separate team/tmb ownership
- no shared permission from the victim toolset to the attacker workflow app
Concrete object values from one v4.14.22 run:
user_a team/tmb: 6a213feed15b4872fc8de667 / 6a213feed15b4872fc8de668
user_b team/tmb: 6a213feed15b4872fc8de66c / 6a213feed15b4872fc8de66d
user_a toolset id: 6a2141e4b81749a85819860e
tool name: sandbox_echo
combined tool id: http-6a2141e4b81749a85819860e/sandbox_echo
user_b workflow app id: 6a2141e4b81749a858198632
These ids are not required to reproduce the issue; they are included so the maintainer can see the exact relationship tested.
Reproduction Steps
1. Create the actors
Create two normal users:
user_a: victim toolset owner
user_b: attacker workflow owner
They should not share the target app/toolset permission.
2. Create a private HTTP toolset as user_a
Create an HTTP toolset owned by user_a with a tool named sandbox_echo. Point it to any harmless endpoint controlled by the tester. My local endpoint returned:
{
"ok": true,
"echo": "sandbox",
"mock": "fastgpt-sandbox"
}
3. Create a workflow app as user_b
Create a minimal workflow app owned by user_b.
4. Confirm user_b cannot access user_a's toolset through official routes
Using user_b's session, verify that the normal routes deny access to user_a's toolset:
GET /api/core/app/tool/getPreviewNode?appId=http-<user_a_toolset_id>/sandbox_echo
POST /api/core/app/tool/getVersionList
GET /api/core/app/detail?appId=<user_a_toolset_id>
POST /api/core/app/update?appId=<user_a_toolset_id>
GET /api/core/app/getPermission?appId=<user_a_toolset_id>
Observed result from my run:
getPreviewNode as user_b: denied with statusText=unAuthApp, code=502001
getVersionList as user_b: denied with statusText=unAuthApp, code=502001
detail as user_b: denied with statusText=unAuthApp, code=502001
update as user_b: denied with statusText=unAuthApp, code=502001
getPermission as user_b: HTTP 200, but hasReadPer=false, hasWritePer=false, hasManagePer=false, isOwner=false
As a positive control, the same preview/detail/version/permission checks succeeded as user_a.
5. Save a crafted workflow node as user_b
Save or publish user_b's workflow app with an HTTP tool node whose saved configuration contains:
toolConfig.httpTool.toolId = http-<user_a_toolset_id>/sandbox_echo
The minimal graph used in my run was:
workflowStartNodeId -> p0aCrossHttpTool -> p0aAnswer
The save request returned HTTP 200 / JSON code 200 even though the referenced toolset belonged to user_a.
6. Confirm the crafted reference was persisted
After saving, the workflow app owned by user_b contained:
containsCrossOwnedToolId=true
crossOwnedToolIdMatches=["http-6a2141e4b81749a85819860e/sandbox_echo"]
The latest published workflow version also contained the same cross-owned id. Ownership was different:
user_a toolset team/tmb: 6a213feed15b4872fc8de667 / 6a213feed15b4872fc8de668
user_b app team/tmb: 6a213feed15b4872fc8de66c / 6a213feed15b4872fc8de66d
7. Trigger workflow runtime as user_b
Call the chat runtime against user_b's workflow app:
POST /api/v2/chat/completions
Observed result:
HTTP status: 200
executed node: p0aCrossHttpTool
executed node name: Cross-owned HTTP sandbox_echo
tool result: {"ok":true,"echo":"sandbox","mock":"fastgpt-sandbox"}
8. Re-run the official negative controls
After runtime execution, user_b was still denied by the official toolset access routes. This confirms that the runtime path executed a toolset that the normal authorization model still considered inaccessible to user_b.
Version Matrix
I repeated the same test flow across current and historical local sandboxes:
| Version |
Baseline |
Negative controls |
Owner positive control |
Crafted save |
Runtime execution |
Result |
v4.14.17 |
pass |
pass |
pass |
pass |
pass |
affected |
v4.14.22 |
pass |
pass |
pass |
pass |
pass |
affected |
v4.14.23 |
pass |
pass |
pass |
pass |
pass |
affected |
v4.15.0-beta3 |
pass |
pass |
pass |
pass |
pass |
affected |
For v4.14.23 and v4.15.0-beta3, the summary values were:
db_contains_cross_owned=true
ownership_different=true
runtime_status_code=200
runtime_contains_mock=true
t1_baseline_pass=true
t2_negative_pass=true
t3_positive_pass=true
t4_crafted_save_pass=true
t5_runtime_pass=true
t6_separation_pass=true
Additional Impact Test: Credentialed External Mock
This section is optional impact context. It is not needed for the root authorization bug.
I configured user_a's private HTTP toolset with a synthetic header credential and pointed it at a local mock external SaaS service:
POST /tenant-a/private-record
POST /tenant-a/update-state
GET /tenant-a/state
The mock rejected direct requests without the expected header. It did not log or return the secret value.
Observed on both v4.14.23 and v4.15.0-beta3:
direct no-credential access: denied
official toolset access by user_b: denied
saved crafted references by user_b: succeeded
runtime private record read: saw_expected_header=true
external private_record_reads: 0 -> 1
runtime update: saw_expected_header=true
external update_count: 0 -> 1
secret value written to evidence: false
If the project treats that local mock as a separate authorization scope, the same root bug has a stronger cross-scope impact. If not, the conservative in-product impact still stands.
Root Cause
The likely issue is that the save/update/publish path authorizes only the workflow app being edited. It does not validate every referenced toolConfig.httpTool.toolId against the editor's permission on the referenced parent toolset.
At runtime, FastGPT trusts the persisted combined tool id, loads the referenced parent toolset version, and executes the saved HTTP tool configuration. That runtime resolution does not appear to re-authorize the referenced toolset for the app/actor context that is executing the workflow.
The normal preview/detail/version/permission routes do perform authorization checks, which is why they fail closed for user_b. The missing check is specifically in the saved workflow reference and runtime path.
The PoC does not rely on any direct URL/schema/tool-run primitive. I did not use:
POST /api/core/app/httpTools/runTool
/api/core/app/httpTools/getApiSchemaByUrl
/api/core/app/mcpTools/getTools
/api/core/app/mcpTools/runTool
cloud metadata targets
real intranet hosts
third-party systems
The demonstrated issue is that a saved workflow can reference and execute a private toolset object that the attacker cannot access through official routes.
CVSS
Conservative score:
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:H/A:N = 5.9 Medium
Rationale:
AV:N: the behavior is reachable through FastGPT HTTP APIs.
AC:H: this PoC assumes the attacker knows the target combined tool id and tool name.
PR:L: the attacker needs a normal authenticated account.
UI:N: the victim does not need to interact after the attacker has the target reference.
S:U: conservative framing treats the impact as inside FastGPT's authorization boundary.
C:L: the attacker can receive output from the invoked private HTTP tool.
I:H: the attacker can trigger another user's private tool capability.
A:N: no availability impact was demonstrated.
Conditional score if the credentialed external mock is treated as a changed scope:
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:H/A:N = 7.1 High
Suggested Fix
I suggest fixing this in both places:
- Save/update/publish validation:
Reject workflow nodes whose HTTP/MCP combined tool ids reference a parent app/toolset the editor cannot read or use.
- Runtime validation:
Before resolving http-<parentAppId>/<toolName> or mcp-<parentAppId>/<toolName>, re-authorize the referenced parent toolset against the current execution context or a durable saved grant.
- Data model hardening:
Prefer storing normalized, grant-backed tool references rather than trusting raw combined ids submitted in workflow JSON.
Regression test suggestion:
user_a owns a private HTTP toolset
user_b owns a workflow app
user_b cannot preview/detail/version/read user_a's toolset
user_b saves a node with http-<user_a_toolset_id>/<tool_name>
save is rejected, or runtime refuses execution
user_a's own workflow/toolset use still works
legitimately shared toolsets still work
Summary
An authenticated FastGPT user can save a workflow node that points to another user's private HTTP toolset by using a crafted saved tool id such as:
The normal toolset routes correctly deny access to that toolset. For example, the attacker cannot preview it, read its detail, list its versions, update it, or obtain read/write/manage permission. The problem is that the workflow save/runtime path does not apply the same authorization check to the referenced toolset. After the crafted workflow is saved,
/api/v2/chat/completionsresolves the saved reference and executes the victim-owned HTTP tool.I reproduced this on:
v4.14.17v4.14.22v4.14.23v4.15.0-beta3I am reporting this as
CWE-863: Incorrect Authorization. It is not a direct SSRF report and it is not an arbitrary code execution report.Impact
The attacker can cause FastGPT to execute a private HTTP toolset that belongs to another user/team. In the basic PoC, the attacker receives the response of a victim-owned HTTP tool. In an additional local-only impact test, the victim toolset carried a synthetic credential for a mock external SaaS endpoint. The attacker still had no official toolset permission, but the saved workflow runtime used the victim toolset's credentialed capability to read a tenant-private marker and update external mock state.
The external-service test used only a local mock service and a synthetic credential. No real third-party service, cloud metadata endpoint, intranet host, production system, or real secret was used.
Security Boundary
The crossed boundary is the ownership and authorization boundary between workflow apps and private HTTP toolsets.
In the PoC:
user_aowns a private HTTP toolset.user_bowns a separate workflow app.user_bhas no permission onuser_a's toolset.user_a's toolset whenuser_bsaves a crafted reference inuser_b's own workflow.This affects another user, not only the reporter's own account.
Preconditions
http-<toolset_app_id>/<tool_name>.I am not claiming low attack complexity for identifier discovery yet. I tested a possible product collaboration path, but my local OSS build did not expose the formal product collaboration API, so I am keeping
AC:H.Environment Used
Main local reproduction:
http://127.0.0.1:3300Concrete object values from one
v4.14.22run:These ids are not required to reproduce the issue; they are included so the maintainer can see the exact relationship tested.
Reproduction Steps
1. Create the actors
Create two normal users:
They should not share the target app/toolset permission.
2. Create a private HTTP toolset as
user_aCreate an HTTP toolset owned by
user_awith a tool namedsandbox_echo. Point it to any harmless endpoint controlled by the tester. My local endpoint returned:{ "ok": true, "echo": "sandbox", "mock": "fastgpt-sandbox" }3. Create a workflow app as
user_bCreate a minimal workflow app owned by
user_b.4. Confirm
user_bcannot accessuser_a's toolset through official routesUsing
user_b's session, verify that the normal routes deny access touser_a's toolset:Observed result from my run:
As a positive control, the same preview/detail/version/permission checks succeeded as
user_a.5. Save a crafted workflow node as
user_bSave or publish
user_b's workflow app with an HTTP tool node whose saved configuration contains:The minimal graph used in my run was:
The save request returned HTTP 200 / JSON code 200 even though the referenced toolset belonged to
user_a.6. Confirm the crafted reference was persisted
After saving, the workflow app owned by
user_bcontained:The latest published workflow version also contained the same cross-owned id. Ownership was different:
7. Trigger workflow runtime as
user_bCall the chat runtime against
user_b's workflow app:Observed result:
8. Re-run the official negative controls
After runtime execution,
user_bwas still denied by the official toolset access routes. This confirms that the runtime path executed a toolset that the normal authorization model still considered inaccessible touser_b.Version Matrix
I repeated the same test flow across current and historical local sandboxes:
v4.14.17v4.14.22v4.14.23v4.15.0-beta3For
v4.14.23andv4.15.0-beta3, the summary values were:Additional Impact Test: Credentialed External Mock
This section is optional impact context. It is not needed for the root authorization bug.
I configured
user_a's private HTTP toolset with a synthetic header credential and pointed it at a local mock external SaaS service:The mock rejected direct requests without the expected header. It did not log or return the secret value.
Observed on both
v4.14.23andv4.15.0-beta3:If the project treats that local mock as a separate authorization scope, the same root bug has a stronger cross-scope impact. If not, the conservative in-product impact still stands.
Root Cause
The likely issue is that the save/update/publish path authorizes only the workflow app being edited. It does not validate every referenced
toolConfig.httpTool.toolIdagainst the editor's permission on the referenced parent toolset.At runtime, FastGPT trusts the persisted combined tool id, loads the referenced parent toolset version, and executes the saved HTTP tool configuration. That runtime resolution does not appear to re-authorize the referenced toolset for the app/actor context that is executing the workflow.
The normal preview/detail/version/permission routes do perform authorization checks, which is why they fail closed for
user_b. The missing check is specifically in the saved workflow reference and runtime path.The PoC does not rely on any direct URL/schema/tool-run primitive. I did not use:
The demonstrated issue is that a saved workflow can reference and execute a private toolset object that the attacker cannot access through official routes.
CVSS
Conservative score:
Rationale:
AV:N: the behavior is reachable through FastGPT HTTP APIs.AC:H: this PoC assumes the attacker knows the target combined tool id and tool name.PR:L: the attacker needs a normal authenticated account.UI:N: the victim does not need to interact after the attacker has the target reference.S:U: conservative framing treats the impact as inside FastGPT's authorization boundary.C:L: the attacker can receive output from the invoked private HTTP tool.I:H: the attacker can trigger another user's private tool capability.A:N: no availability impact was demonstrated.Conditional score if the credentialed external mock is treated as a changed scope:
Suggested Fix
I suggest fixing this in both places:
Reject workflow nodes whose HTTP/MCP combined tool ids reference a parent app/toolset the editor cannot read or use.
Before resolving
http-<parentAppId>/<toolName>ormcp-<parentAppId>/<toolName>, re-authorize the referenced parent toolset against the current execution context or a durable saved grant.Prefer storing normalized, grant-backed tool references rather than trusting raw combined ids submitted in workflow JSON.
Regression test suggestion: