GHSA-j4hj-7hfh-g2f4 praisonai: recipe serve auth middleware silently disables itself when no secret is set
praisonai: `recipe serve` authentication middleware silently disables itself when no secret is set**Researcher:** Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research
**Target:** https://github.com/MervinPraison/PraisonAI**Package:** `praisonai` on PyPI
**Version tested:** 4.6.48.
**File:** `praisonai/recipe/serve.py` (sha256 `491bf8f29e399418260810ba4bf0f6802c6e4aa675628e2be68a9726c15d9b23`).TL;DR`praisonai/recipe/serve.py:312-410` defines two auth middlewares (`APIKeyAuthMiddleware`, `JWTAuthMiddleware`). Both contain the same "fail open when the secret is unset" branch at the top of their `dispatch`:The realistic mis-deploy:operator sets `auth: api-key` (or `auth: jwt`) in their recipe YAML, expecting that line alone to enable auth,operator does not set the corresponding `api_key:` / `jwt_secret:` value in the same YAML, ANDoperator does not export `PRAISONAI_API_KEY` / `PRAISONAI_JWT_SECRET` in the environment.The middleware silently treats every request as authenticated and forwards it to the recipe-execution route.Combined with the praisonai jobs API having zero auth (a separate finding), operators who paid attention to "I have to set `auth: api-key` to lock this down" still don't get auth on the recipe-serve surface unless they also remember the secret.Root causeEmpirical verification`poc/poc.py`:Imports the installed praisonai 4.6.48 `praisonai.recipe.serve` module (sha256 `491bf8f29e399418260810ba4bf0f6802c6e4aa675628e2be68a9726c15d9b23`).Clears `PRAISONAI_API_KEY` / `PRAISONAI_JWT_SECRET` env vars to simulate the mis-deploy.Calls `serve.create_auth_middleware('api-key', api_key=None, jwt_secret=None)` and instantiates the returned middleware.Builds a Starlette `Request` for `/runs` (the recipe-execution path) with empty headers — no `X-API-Key`, no `Authorization`.`await middleware.dispatch(request, fake_call_next)` returns the sentinel `'REACHED-DOWNSTREAM (path=/runs)'` from the fake `call_next` — proving the middleware passed the request through without authenticating.Repeats the test for `auth_type='jwt'` — same bypass on the JWT path.Run log (`poc/run-log.txt`) summary:ImpactThe recipe-serve surface runs agentic workflows — same execution posture as `praisonai/jobs/server.py` but separately configured / separately reached. Unauth access on this surface yields:Trigger arbitrary recipe executions, passing attacker-controlled inputs and configurations.Read the inputs / outputs of in-flight recipes — the operator's prompts and the LLM responses.In some deployments, the recipe execution surface is wired to tools (browser automation, file-system writes, code execution). Reaching those tools without auth is a direct RCE path.Anchors`praisonai/recipe/serve.py:325-333` — `APIKeyAuthMiddleware.dispatch` silent-bypass branch.`praisonai/recipe/serve.py:352-355` — `JWTAuthMiddleware.dispatch` silent-bypass branch.`praisonai/recipe/serve.py:688-694` — call site:Suggested fixWhen the operator has signalled "I want auth", refuse to start without the corresponding secret rather than silently degrading:This is the same pattern the sibling `praisonai.gateway` server applies in `assert_external_bind_safe` at `praisonai/gateway/auth.py:48-54` — refuse-to-start on external bind without an auth token. The recipe-serve surface should do the same.Steps to reproduceClone the target: `git clone --depth 1 https://github.com/MervinPraison/PraisonAI`Run the proof of concept (`poc.py`) against the cloned source.Observe the result shown under *Verified result* below.Proof of concept`poc.py`Verification harness (executed against the cloned repo)This drives the unmodified upstream code rather than a reproduction.Verified resultThis PoC was executed against the live upstream code; captured output:CreditKai Aizen — SnailSploit (@SnailSploit). Adversarial & Offensive Security Research.
CVSS v3.1 score: 9.8