# praisonai-platform: default JWT signing secret 'dev-secret-change-me' enables token forgery

praisonai-platform falls back to public JWT secret 'dev-secret-change-me' when env vars unset, letting attackers forge admin tokens accepted by all authenticated routes.

- Payout: Research
- Severity: critical
- Program: praisonai-platform
- Found by: SnailSploit
- Disclosed: 2026-06-18
- CVE: CVE-2026-57147
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-cwj8-7gp2-ggcw
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-cwj8-7gp2-ggcw
- Tags: area:web domain:api domain:web impact:ato impact:privesc kind:research platform:ghsa sev:critical tech:jwt tech:python technique:source-review vuln:auth-bypass vuln:misconfiguration vuln:secrets-leak

**Target:** FastAPI auth service JWT signing (auth_service.py)
**Root cause:** JWT_SECRET defaults to the public literal 'dev-secret-change-me'; the startup guard only fires when PLATFORM_ENV is explicitly non-'dev', so operators who set nothing silently run with the public secret.
**Steps:**
1. Install praisonai-platform==0.1.4 with PLATFORM_JWT_SECRET and PLATFORM_ENV unset.
2. Observe JWT_SECRET == 'dev-secret-change-me' and the line-33 guard does not raise (PLATFORM_ENV defaults to 'dev').
3. Forge a token: `jwt.encode({'sub':'admin-user-id-attacker-chose','email':'admin@example.com',...}, 'dev-secret-change-me', algorithm='HS256')`.
4. Call AuthService._verify_token(forged_token) or send as Bearer to any Depends(get_current_user) route; identity.id/email reflect attacker-chosen values.
**How found:** Read the startup guard at auth_service.py:33 and noticed it checks PLATFORM_ENV != 'dev' rather than requiring explicit production opt-in; realized the forgot-to-set-both-env-vars case (the exact case the guard targets) evaluates the second leg False and skips the guard.
**Exploitation:** Unauthenticated attacker mints an HS256 JWT signed with the public default secret carrying any sub/email, sends it as Bearer to any route behind get_current_user; workspace membership checks pass if sub matches a known member id (member/workspace ids leak via activity/labels endpoints).
**Escalation:** Forged sub set to a known workspace member id bypasses require_workspace_member, granting access to that workspace's projects, issues, agents and labels.
**Insight:** When auditing default-secret guards, check the polarity of the condition: a guard that requires the operator to explicitly declare 'production' cannot catch the forgot-to-configure deployment, which is the common real-world case. Test with all env vars unset, not just the secret.

