GHSA-5qw8-f2g9-ff29: PraisonAI recipe serve Typer command bypasses the non-localhost authentication guard
PraisonAI `recipe serve` Typer command bypasses the non-localhost authentication guardSummaryPraisonAI's installed console entrypoint is Typer-first. In current releases,
the `recipe` command is registered in the Typer app and
`praisonai recipe serve` dispatches to the deprecated Typer command in
`src/praisonai/praisonai/cli/commands/recipe.py`.That Typer command can start the Recipe HTTP server on a non-localhost
interface with no authentication:It prints a deprecation warning, then launches the server with:Because `config.auth` is absent, `create_app()` does not attach the API-key or
JWT middleware. Unauthenticated requests can then reach the recipe API and, when
enabled, `/admin/reload`.This is an incomplete hardening / sibling-callsite issue. The legacy feature
handler in `src/praisonai/praisonai/cli/features/recipe.py` rejects the same
non-localhost/no-auth combination, and current `create_auth_middleware()` now
fails closed if API-key/JWT auth is selected without a secret. The installed
Typer command bypasses both expectations by never requiring or setting `auth`.Affected productRepository: `MervinPraison/PraisonAI`Package: `praisonai`Component:`src/praisonai/praisonai/__main__.py``src/praisonai/praisonai/cli/app.py``src/praisonai/praisonai/cli/commands/recipe.py``src/praisonai/praisonai/cli/features/recipe.py``src/praisonai/praisonai/recipe/serve.py`Confirmed affected:Suggested affected range: `praisonai >= 4.5.112, <= 4.6.58`.The lower bound is conservative and based on sampled tags. Maintainers should
confirm the exact introduction point before publishing a final range.Root causeThe installed entrypoint routes registered Typer commands before falling back
to the legacy dispatcher:`cli/app.py` registers `commands.recipe` as the `recipe` Typer command:The deprecated Typer `recipe serve` implementation accepts a remote host,
defaults CORS to `*`, and only enables authentication when `--api-key` is
explicitly provided:There is no equivalent to the hardened non-localhost guard in the legacy
feature handler:The Recipe server only installs auth middleware when `config["auth"]` is set:On current `v4.6.58`, the selected-auth paths fail closed correctly:`auth=api-key` with no key returns `503`.`auth=api-key` with a key but no request header returns `401`.The vulnerable Typer path does not select auth at all.Local-only PoVRun from the harness checkout:The PoV does not bind a socket. It monkey-patches the recipe server launcher,
invokes the real `praisonai.__main__.main()` entrypoint with
`recipe serve --host 0.0.0.0 --admin`, captures the launch config, and then
uses Starlette's in-process test client to exercise the resulting app.Observed `v4.6.58` result:Stored evidence:`evidence/current-v4.6.58.json``evidence/version-sweep.tsv`Why this is not intended behaviorThis is not only a disagreement about whether operators should configure auth.PraisonAI's current security documentation says recent hardening changed API
servers so anonymous requests return `401` and servers bind to `127.0.0.1` by
default. Recipe server docs say `auth: api-key` should be used for production,
admin endpoints require auth, and public servers should not run without
authentication.The implementation also shows the intended boundary:`create_auth_middleware()` now returns `503` if API-key/JWT auth is selected
without a secret.`RecipeHandler.cmd_serve()` refuses non-localhost binding when `auth` is
`none`.The vulnerable Typer command is marked deprecated and tells users to use the
newer command, but the installed entrypoint still routes `praisonai recipe`
to that Typer command before the legacy handler can enforce the guard.The official local HTTP sidecar docs describe the sidecar as communicating over
localhost and "no external network required", but the Docker example still uses:That command exposes the Typer path above and does not enable auth, even if
`PRAISONAI_API_KEY` is present in the environment, because this path only sets
`auth` when `--api-key` is passed or a config file sets `auth`.ImpactIf an operator follows the vulnerable command path on a reachable interface,
any network caller that can reach the Recipe HTTP server can access recipe
runner endpoints without credentials.Affected endpoints include:`GET /v1/recipes``POST /v1/recipes/run``POST /v1/recipes/stream``POST /v1/recipes/validate`optional `POST /admin/reload` when admin endpoints are enabledThe exact impact depends on configured recipes and deployment context. At a
minimum, an attacker can enumerate recipes and trigger recipe validation or
execution flows intended for local or authenticated callers. In deployments
with powerful recipes, tool-enabled recipes, or admin endpoints, this can cause
unauthorized workflow execution, model/API spend, state changes, or recipe
registry reload operations.This report does not claim arbitrary code execution by default.Suggested fixPrefer one canonical Recipe server CLI path and enforce the same preflight for
every wrapper.Recommended changes:Remove or hard-disable the deprecated Typer `praisonai recipe serve` command,
or make it delegate to the hardened `RecipeHandler.cmd_serve()` code path.Add the same non-localhost/no-auth guard to `cli/commands/recipe.py`.Treat `PRAISONAI_API_KEY` as a secret only when `auth=api-key` is selected;
do not rely on the env var's presence alone unless the command also enables
auth explicitly.Fix the deprecated command's help examples so remote binding always includes
auth.Consider changing `--cors` default from `*` to no CORS or localhost origins.Add regression tests that invoke the installed `praisonai.__main__.main()`
entrypoint, not only the legacy feature handler:`praisonai recipe serve --host 0.0.0.0` fails before launch unless auth is
selected and configured;`praisonai recipe serve --host 0.0.0.0 --admin` cannot expose
`/admin/reload` without auth;selected but misconfigured auth still returns `503`;configured auth with no header returns `401`.
CVSS v3.1: 8.2