GitHub Advisoryadvisory · PyPI
GHSA-r7v3-x45f-g7hp PraisonAI: [Auth Bypass] `praisonai serve agents --api-key` is silently ignored — agent-invocation routes (`POST /agents`, `POST /agents/{agent_name}`) run unauthenticated
HIGH
Official page ↗
- Aliases
- CVE-2026-55538
- Ecosystem
- PyPI
- Packages
- PraisonAI
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
- Weaknesses
- CWE-306
- Reviewed
- 2026-08-25
Affected packages
• PyPI PraisonAI: < 4.6.58 → fixed in 4.6.58
Description
Summary
`praisonai serve agents` exposes HTTP routes that invoke registered agents. The CLI advertises `--api-key` with help text "API key for authentication", parses it, and forwards it into `ServeHandler`. But `_create_agents_app()` never reads `config["api_key"]` again and installs no auth dependency or middleware on its direct routes. The configured key is a no-op flag.
As a result, an unauthenticated network caller can invoke exposed agents (`POST /agents` and `POST /agents/{agent_name}`) even when the operator passed `--api-key`. Requests with no credentials, a wrong `Authorization: Bearer`, a wrong `X-API-Key`, or an empty bearer all reach `agent.start()`.
The failure is made sharper by the fact that a working auth dependency already exists in the same module — `praisonai.api.agent_invoke.verify_token` guards every `/api/v1/...` route with `Depends(verify_token)` and is mounted into the very same app. The direct n8n-compat routes simply do not use it.
Technical Detail
Source-to-sink trace
1. CLI advertises and forwards `--api-key`:
```python
cli/commands/serve.py
@app.command("agents")
def serve_agents(..., api_key: Optional[str] = typer.Option(None, "--api-key", help="API key for authentication")):
...
if api_key:
args.extend(["--api-key", api_key])
exit_code = handle_serve_command(args)
```
2. `cmd_agents()` parses `api_key` into the spec — and that is the last time it is touched:
```python
cli/features/serve.py — cmd_agents()
spec = { ..., "api_key": {"default": None} }
parsed = self._parse_args(args, spec)
app = self._create_agents_app(parsed)
```
A grep of the entire `cli/features/serve.py` for `api_key` returns only the two `spec` entries (`cmd_agents` line ~199 and `cmd_unified` line ~847). `config["api_key"]` is never read inside `_create_agents_app()` / `_create_unified_app()`; it is never compared, and no dependency is attached.
3. `_create_agents_app()` imports `FastAPI, HTTPException, Request` — no `Depends`, no `Header`, no auth middleware. Every `HTTPException` raised in the agents routes is `400`/`404`/`500` (validation / not-found / execution error); none is `401`.
4. Sink — unauthenticated request reaches `agent.start()`:
```python
cli/features/serve.py
@app.post("/agents/{agent_name}") # n8n compatibility route
async def invoke_single_agent(agent_name: str, request: Request):
body = await request.json()
query = body.get("query", "") or body.get("message", "")
...
agent = agent_invoke.get_agent(agent_name)
result = await loop.run_in_executor(None, agent.start, query) # no auth anywhere above
return {"response": str(result)}
@app.post(path) # default path "/agents"
async def invoke_agents(request: Request, query_data: AgentQuery = None):
... agent.start(query) ...
```
The auth dependency exists — it just isn't applied here
`_create_agents_app()` mounts the `agent_invoke` router into the same app:
```python
cli/features/serve.py
if getattr(agent_invoke, 'FASTAPI_AVAILABLE', False) and hasattr(agent_invoke, 'router'):
app.include_router(agent_invoke.router)
```
That router properly authenticates every sensitive route:
```python
api/agent_invoke.py
CALL_SERVER_TOKEN = os.getenv('CALL_SERVER_TOKEN')
async def verify_token(request, authorization=Header(None)) -> None:
...
if token != CALL_SERVER_TOKEN:
raise HTTPException(status_code=401, detail="Unauthorized")
@router.get("/api/v1/agents")
async def list_agents(_: None = Depends(verify_token)): ... # and register/unregister/info all use it
```
So in the same process `GET /api/v1/agents` returns `401` without a token, while `POST /agents/{agent_name}` returns `200`. Note also that `verify_token` reads the `CALL_SERVER_TOKEN` env var — not the CLI `--api-key` — so the CLI option feeds no auth path at all.
Trigger conditions
```
praisonai serve agents --file agents.yaml --host 0.0.0.0 --port 8765 --api-key expected-secret
POST /agents/{agent_name} body {"query":"..."} with no / wrong / empty credentials
```
Proof of Concept
Built the real `_create_agents_app()` and exercised it over HTTP via FastAPI `TestClient`. Only `praisonaiagents.Agent` is stubbed (`.start()` returns `EXEC:`), so no real LLM/credentials. `CALL_SERVER_TOKEN=expected-secret` was set so the sibling `/api/v1` router is genuinely armed — making the contrast explicit.
```
Operator started with: --api-key expected-secret (CALL_SERVER_TOKEN also set)
== Sibling /api/v1 route WITH Depends(verify_token) ==
GET /api/v1/agents [no creds ] -> HTTP 401
GET /api/v1/agents [wrong bearer] -> HTTP 401
== Direct agent-invocation route (the bug) ==
POST /agents/owned [no creds ] -> HTTP 200 {'response': 'EXEC:hello'}
POST /agents/owned [wrong bearer ] -> HTTP 200 {'response': 'EXEC:hello'}
POST /agents/owned [wrong x-api-key] -> HTTP 200 {'response': 'EXEC:hello'}
POST /agents/owned [empty bearer ] -> HTTP 200 {'response': 'EXEC:hello'}
POST /agents [no creds ] -> HTTP 200 {'response': 'EXEC:hi'}
```
The auth mechanism works for `/api/v1` (401) and is entirely absent on the direct `/agents` routes (200), despite `--api-key` being configured.
Equivalent HTTP trigger in a fully installed environment
```bash
praisonai serve agents --file agents.yaml --host 0.0.0.0 --port 8765 --api-key expected-secret
curl -sS -X POST http://TARGET:8765/agents/owned \
-H 'Content-Type: application/json' --data-binary '{"query":"hello"}'
-> 200 {"response":"..."} (expected: 401 Unauthorized)
```
Impact
• Direct primitive: unauthenticated agent invocation despite a configured API key.
• Misleading control (aggravating): because the CLI advertises `--api-key` as authentication, operators may deliberately expose the service (e.g. `--host 0.0.0.0`, reverse proxy, n8n integration) believing it is protected, increasing the real-world likelihood of exposure.
• Downstream: exposed agents commonly hold LLM provider credentials, RAG/memory, browser/search, MCP, or shell/file tools; the bypass lets an attacker drive those capabilities. Baseline impact is unauthorized LLM cost + access to agent responses.
Suggested Mitigation
• When `config["api_key"]` is set, build a shared auth dependency and attach it to every agent-invocation / state-changing route in `_create_agents_app()` and `_create_unified_app()` (`dependencies=[Depends(verify)]`).
• Reuse / unify with the existing `verify_token` so the direct `/agents` routes and the `/api/v1` routes share one mechanism, and wire the CLI `--api-key` into that mechanism (today it feeds nothing; `verify_token` reads `CALL_SERVER_TOKEN`).
• Use constant-time comparison (`hmac.compare_digest`); `verify_token` currently uses `!=`.
• Update discovery metadata from `auth_modes=["none"]` to `["api-key","bearer"]` for protected endpoints.
• Regression tests next to `tests/unit/test_serve_unified.py`: `_create_agents_app({"api_key":"secret",...})` → `POST /agents/{name}` with no creds / wrong `Authorization` / wrong `X-API-Key` returns `401`; correct key succeeds.
```python
import hmac
from fastapi import Header, HTTPException, Depends
def _auth_dependency(expected_key: str):
async def verify(authorization: str | None = Header(None),
x_api_key: str | None = Header(None, alias="X-API-Key")):
token = x_api_key
if authorization and authorization.startswith("Bearer "):
token = authorization[7:]
if not token or not hmac.compare_digest(token, expected_key):
raise HTTPException(status_code=401, detail="Unauthorized")
return Depends(verify)
```
Related
Alias · CVE (1)
- CVE-2026-55538 PraisonAI is a multi-agent teams system. vulnerability
Weakness · CWE (1)
- CWE-306 Missing Authentication for Critical Functionality weakness