# PraisonAI recipe serve Typer command bypasses the non-localhost authentication guard

Deprecated Typer `recipe serve` command in PraisonAI binds 0.0.0.0 with no auth, exposing recipe run/validate and /admin/reload endpoints to unauthenticated network callers.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by: rexpository
- Disclosed: 2026-06-18
- CVE: CVE-2026-56836
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-5qw8-f2g9-ff29
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-5qw8-f2g9-ff29
- Tags: area:ai domain:ai domain:web impact:internal-access kind:research platform:ghsa sev:high tech:python technique:legacy-endpoint technique:source-review vuln:access-control vuln:auth-bypass vuln:misconfiguration

**Target:** Recipe HTTP server CLI entrypoint (Typer command)
**Root cause:** The installed entrypoint routes `recipe` to a deprecated Typer command that never sets `config.auth`, so create_app() skips auth middleware; the hardened non-localhost/no-auth guard exists only in the legacy feature handler that is never reached.
**Steps:**
1. Run `praisonai recipe serve --host 0.0.0.0 --admin` (deprecation warning printed, server launches with config `{"cors_origins":"*","enable_admin":true}` and no `auth` key)
2. Observe create_app() installs no API-key/JWT middleware because `config.get("auth")` is unset
3. Send unauthenticated `GET /openapi.json` -> 200
4. Send unauthenticated `POST /admin/reload` -> 200
5. Contrast: legacy RecipeHandler.cmd_serve() exits 4 for non-localhost+auth=none; auth=api-key without secret returns 503, with secret but no header returns 401
**How found:** Noticed the installed entrypoint dispatches registered Typer commands before the legacy dispatcher, so the deprecated `recipe` Typer command shadows the hardened feature handler; compared the two code paths and found the Typer one never sets `auth`, then confirmed via in-process Starlette test client.
**Exploitation:** Any network caller reaching the bound interface can hit recipe runner endpoints (GET /v1/recipes, POST /v1/recipes/run, /stream, /validate) and /admin/reload without credentials, enumerating and executing recipes and triggering registry reloads.
**Escalation:** Enabling --admin exposes /admin/reload; tool-enabled or powerful recipes allow unauthorized workflow execution, model/API spend and state changes.
**Insight:** When a project migrates CLI frameworks, check whether the new dispatcher shadows hardened legacy handlers: sibling callsites implementing the same feature can silently skip security preflight checks. Test the installed entrypoint, not just the internal handler.

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

**PoC command invoking vulnerable recipe serve**

```
uv run \
  --with starlette --with httpx --with typer --with rich --with pyyaml \
  --with sse-starlette --with click --with python-dotenv \
  python submission-bundle/praisonai-prai-cand-016-recipe-serve-typer-auth-bypass/poc/pov_prai_cand_016_recipe_serve_typer_auth_bypass.py \
  --repo artifacts/repos/praisonai-v4.6.58 \
  --label v4.6.58
```

**Vulnerable Typer serve command with no auth**

```
host: str = typer.Option("127.0.0.1", "--host", "-h", ...)
api_key: str = typer.Option(None, "--api-key", ...)
cors: str = typer.Option("*", "--cors", ...)
admin: bool = typer.Option(False, "--admin", ...)
...
serve_config = {}
...
if api_key:
    serve_config["api_key"] = api_key
    serve_config["auth"] = "api-key"
if cors:
    serve_config["cors_origins"] = cors
if admin:
    serve_config["enable_admin"] = True
...
serve(host=host, port=port, reload=reload, config=serve_config, workers=workers)
```

**Observed bypass result JSON**

```
{
  "candidate": "PRAI-CAND-016",
  "entrypoint_exit_code": 0,
  "typer_recipe_command_registered": true,
  "captured_launch": {
    "host": "0.0.0.0",
    "port": 8765,
    "config": {
      "cors_origins": "*",
      "enable_admin": true
    }
  },
  "bypass": {
    "admin_reload": {
      "path": "/admin/reload",
      "status": 200
    },
    "openapi": {
      "path": "/openapi.json",
      "status": 200
    }
  },
  "controls": {
    "auth_api_key_no_secret": {
      "admin_reload": {
        "status": 503
      }
    },
    "auth_api_key_no_header": {
      "admin_reload": {
        "status": 401
      }
    }
  },
  "feature_handler_nonlocalhost_noauth_exit": 4,
  "auth_fail_closed_current_control": true,
  "ok": true
}
```

