Skip to content

S3 presign/read handlers do not bind the object key to the caller's team (cross-team file disclosure)

High
c121914yu published GHSA-6rxv-p43w-mmx5 Jul 1, 2026

Package

fastgpt (labring)

Affected versions

<4.15.0-beta5

Patched versions

v4.15.0-beta5

Description

Summary

Two file handlers authorize an unrelated resource (an app, or a different dataset the attacker owns) and then sign or read an S3 object using a key taken directly from the request, without checking that the key belongs to the caller's team. Because S3 object keys are global within the bucket and carry the tenant id only as a path segment, an attacker can supply another team's key and obtain its file contents. The chat-file presign endpoint is reachable even by an unauthenticated public share-link visitor, yielding cross-team disclosure of uploaded chat files. The dataset preview endpoint yields cross-team disclosure of dataset file contents for any authenticated user with write access to any dataset.

Details

The correct pattern exists in the codebase, projects/app/src/pages/api/core/dataset/file/getSearchTestImagePreviewUrls.ts:23-32: it derives teamId from authDataset and then .filter((key) => isS3ObjectKey(key, 'temp') && key.startsWith(\temp/${teamId}/`))` before signing. The two handlers below omit that key-to-team binding.

  1. Chat file presign (unauthenticated cross-team read). projects/app/src/pages/api/core/chat/file/presignChatFileGetUrl.ts:
const { key, appId, mode, outLinkAuthData } = parseApiInput({ req, bodySchema: PresignChatFileGetUrlSchema }).body;
await authChatCrud({ req, authToken: true, authApiKey: true, appId, ...outLinkAuthData });   // checks access to appId only; never inspects key
const { url } = await getS3ChatSource().createGetChatFileURL({ key, external: true, mode });   // signs whatever key
return url;

authChatCrud only proves the caller can read appId (or holds a valid share link for it; chatId is optional, so an outLink visitor passes). It does not look at key. createGetChatFileURL -> createExternalUrl, packages/service/common/s3/buckets/base.ts:236, signs the provided key with no team/app/chat scoping (proxy mode returns /api/system/file/[jwt] where the JWT only attests the key, not ownership). The request schema is key: z.string().min(1) with no prefix constraint (packages/global/openapi/core/chat/file/api.ts). Chat keys are chat/<appId>/<uId>/<chatId>/<filename> (packages/service/common/s3/utils.ts), so any other team's chat files are reachable by key. The sibling helperBot handler does the right check (core/chat/helperBot/getFilePreviewUrl.ts parses the key and rejects on userId !== uid); this one does not.

  1. Dataset file preview (authenticated cross-team read). projects/app/src/pages/api/core/dataset/file/getPreviewChunks.ts (fileLocal branch): it calls authCollectionFile({ fileId: sourceId, per: OwnerPermissionVal }) on the victim key, then authDataset({ datasetId, per: Write }) on a different dataset the attacker owns, then readDatasetSourceRawText({ teamId: , sourceId: , datasetId: }). authCollectionFile, packages/service/support/permission/auth/file.ts:15-40, only checks that the S3 object exists and then returns new Permission({ role: ReadRoleVal, isOwner: true }) unconditionally, with no team check:
if (isS3ObjectKey(fileId, 'dataset')) {
  const exists = await getS3DatasetSource().isObjectExists(fileId);
  if (!exists) return Promise.reject(CommonErrEnum.fileNotFound);
} else { return Promise.reject('Invalid dataset file key'); }
const permission = new Permission({ role: ReadRoleVal, isOwner: true });   // isOwner always true

The guard if (fileAuthRes.tmbId !== tmbId ...) is ineffective because fileAuthRes.tmbId is the caller's own tmbId. readDatasetSourceRawText -> getDatasetFileRawText (packages/service/common/s3/sources/dataset/index.ts) downloads by key alone. Dataset keys are dataset/<datasetId>/<filename>.

PoC

Chat files (unauthenticated, via any public share link):

POST /api/core/chat/file/presignChatFileGetUrl
{ "appId": "<an app reachable via a public share link>",
  "outLinkAuthData": { "shareId": "<that share id>", "outLinkUid": "<any>" },
  "key": "chat/<victimAppId>/<victimUid>/<victimChatId>/<file>" }

returns a working download URL for another team's chat file.

Dataset files (authenticated user with Write on any dataset):

POST /api/core/dataset/file/getPreviewChunks
{ "type": "fileLocal", "datasetId": "<attacker-owned dataset, Write>", "sourceId": "dataset/<victimDatasetId>/<file>" }

returns parsed raw text/chunks of the victim team's file.

Live-validated (chat-file path): the verbatim createExternalUrl proxy-mode logic (jwtSignS3DownloadToken over { objectKey } with HS256) was executed with a victim chat-file key, and the /api/system/file/[jwt] proxy verification (signature-only) was run against the resulting token. Captured:

[001] minted download token for VICTIM key: chat/victimApp/victimUid/victimChat/secret_contract.pdf
proxy /api/system/file/[jwt] verified -> {"ok":true,"objectKey":"chat/victimApp/victimUid/victimChat/secret_contract.pdf"}
CROSS-TEAM FILE: CONFIRMED (valid signed download URL issued+accepted for another team's file key, no key->team check)

A valid download token was issued and accepted for another team's file key, with no key-to-team binding (the proxy attests only the signature, not ownership). authChatCrud checking only appId is verified at source. (The signing secret is a stand-in; the sign-any-key + verify-signature-only logic is verbatim.) The chat-file path requires only knowledge of the target key; the dataset path additionally requires guessing the dataset key (datasetId is a 24-char ObjectId plus the uploaded filename), which bounds its practical reach.

Impact

Cross-team disclosure of uploaded files. The chat-file presign endpoint requires no authentication beyond a public share link to any one app, so any tenant's chat-uploaded files are readable given their key. The dataset-preview endpoint discloses any team's dataset file contents to an authenticated user holding write access to any dataset, given the file key. Uploaded files commonly contain sensitive documents.

Remediation

Bind the object key to the authorized tenant before signing or reading, as getSearchTestImagePreviewUrls already does. In presignChatFileGetUrl, parse key and require it to match the authorized app and the caller's uId (chat///...), rejecting otherwise. In authCollectionFile, look up the file's owning team and verify it equals the caller's teamId (do not return isOwner:true unconditionally), and have getDatasetFileRawText/readDatasetSourceRawText assert the key is under dataset// for a dataset the caller is authorized on. More generally, every presign/read should validate the key prefix against the caller's teamId.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

CVE ID

CVE-2026-55418

Weaknesses

Authorization Bypass Through User-Controlled Key

The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. Learn more on MITRE.

Credits