GitHub Advisoryadvisory · PyPI

GHSA-3qg8-5g3r-79v5 praisonai-platform: JWT signing key defaults to hardcoded "dev-secret-change-me", allowing token forgery for any user when PLATFORM_ENV is unset

CRITICAL Published 2026-05-29 Modified 2026-05-29 Official page ↗
Aliases
CVE-2026-47410
Ecosystem
PyPI
Packages
praisonai-platform
Severity
Critical
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Weaknesses
CWE-321, CWE-798
Reviewed
2026-05-29

Affected packages

• PyPI praisonai-platform: < 0.1.4 → fixed in 0.1.4

Description

Summary Type: Insecure default cryptographic key. The JWT signing secret defaults to the hardcoded literal `"dev-secret-change-me"` when `PLATFORM_JWT_SECRET` is unset. A safety check exists but only fires when `PLATFORM_ENV != "dev"`; the default value of `PLATFORM_ENV` is `"dev"`, so the check is silently bypassed in any deployment that does not explicitly opt out. The attacker reads the literal from this public source file, mints a JWT with arbitrary `sub` and `email` claims, and authenticates as any existing user (including workspace owners and admins). File: `src/praisonai-platform/praisonai_platform/services/auth_service.py`, lines 25-36 and 114-137. Root cause: the production-mode guard checks `os.environ.get("PLATFORM_ENV", "dev") != "dev"` — but the default is `"dev"`, so a clean deployment that just imports the package and runs `uvicorn praisonai_platform.api.app:app` proceeds with the hardcoded secret. The package documentation does not warn loudly enough that BOTH variables must be set; the guard suppresses itself when either condition is missed. JWT verification at line 129 trusts whatever the token says (`sub`, `email`, `name`) once the HMAC-SHA256 signature validates against the publicly-known secret. Since the verifier accepts forged tokens for any user_id, the attacker becomes that user across every authenticated route. Affected Code File: `src/praisonai-platform/praisonai_platform/services/auth_service.py`, lines 25-36 and 114-137. ```python _DEFAULT_SECRET = "dev-secret-change-me" JWT_SECRET = os.environ.get("PLATFORM_JWT_SECRET", _DEFAULT_SECRET) # str: payload = { "sub": user.id, "email": user.email, "name": user.name, "iat": now, "exp": now + timedelta(seconds=JWT_TTL_SECONDS), } return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) # signs with the hardcoded secret def _verify_token(self, token: str) -> Optional[AuthIdentity]: try: payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) # verifies with the hardcoded secret return AuthIdentity( id=payload["sub"], # `. `_verify_token` decodes the token using `JWT_SECRET = "dev-secret-change-me"`, the HMAC matches, an `AuthIdentity(id="T_id", ...)` is returned. The route resolves the actual `User` row by `User.id == "T_id"` and returns the victim's record. State: attacker is authenticated as the victim. 6. Attacker pivots: `POST /workspaces/{id}/members` to add themselves as owner (chaining with the companion priv-esc advisory becomes redundant — the attacker is already the victim), `PATCH /workspaces/{id}` to flip settings, `DELETE /workspaces/{id}` to wipe data, or simply `GET /workspaces/{id}/issues/...` to exfiltrate everything the victim could read. 7. Final state: full account takeover for any user_id on any deployment that did not explicitly set both `PLATFORM_JWT_SECRET` and `PLATFORM_ENV=production`. No prior auth, no user interaction, no special network position required. Security Impact Severity: sec-critical. CVSS 9.8: network attack, low complexity, no privileges, no user interaction, scope unchanged (the JWT layer is the same component the attacker pivots through), high confidentiality, high integrity, high availability (chaining with `delete_workspace` from the companion advisory). Attacker capability: mint a JWT for any `user_id` on the deployment with the public secret, becoming that user across every authenticated route. No prior authentication required — the attacker only needs the package to be deployed and reachable. This is a pre-auth full account takeover. Preconditions: `praisonai-platform` is deployed without explicitly setting BOTH `PLATFORM_JWT_SECRET` AND `PLATFORM_ENV=`. The default deployment pattern (pip install, `uvicorn ...`) hits this. The attacker needs network reachability to the API. Differential: source-inspection-verified end-to-end. The asymmetry is between the documented intent of the guard (warn in production) and its actual semantics (warn only when the operator sets `PLATFORM_ENV` to a non-"dev" value). With the suggested fix below, the guard fails-closed: any deployment that did not set `PLATFORM_JWT_SECRET` raises at import time, regardless of `PLATFORM_ENV`. The forged-token attack returns `None` from `_verify_token` because the signing key the attacker used (`"dev-secret-change-me"`) no longer matches the deployment's secret. Suggested Fix Fail-closed at import time when the secret is the default, irrespective of `PLATFORM_ENV`. Permit explicit dev-mode opt-in with a separate variable that is NEVER the default. ```diff --- a/src/praisonai-platform/praisonai_platform/services/auth_service.py +++ b/src/praisonai-platform/praisonai_platform/services/auth_service.py @@ -23,12 +23,16 @@ _pwd_context = CryptContext(schemes=["…

Related

Alias · CVE (1)

  • CVE-2026-47410 PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. vulnerability

Weakness · CWE (2)

  • CWE-321 Use of Hard-coded Cryptographic Key weakness
  • CWE-798 Use of Hard-coded Credentials weakness

References