GHSA-r7v3-x45f-g7hp: High severity pip/PraisonAI vulnerability
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 createagentsapp() never reads config["apikey"] 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/{agentname}) 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.agentinvoke.verifytoken guards every /api/v1/... route with Depends(verifytoken) 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 serveagents(..., apikey: Optional[str] = typer.Option(None, "--api-key", help="API key for authentication")): ... if apikey: args.extend(["--api-key", apikey]) exitcode = handleservecommand(args) 2. cmdagents() parses apikey into the spec — and that is the last time it is touched: python cli/features/serve.py — cmdagents() spec = { ..., "apikey": {"default": None} } parsed = self.parseargs(args, spec) app = self.createagentsapp(parsed) A grep of the entire cli/features/serve.py for apikey returns only the two spec entries (cmdagents line ~199 and cmdunified line ~847). config["apikey"] is never read inside createagentsapp() / createunifiedapp(); it is never compared, and no dependency is attached. 3. createagentsapp() 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/{agentname}") # n8n compatibility route async def invokesingleagent(agentname: str, request: Request): body = await request.json() query = body.get("query", "") or body.get("message", "") ... agent = agentinvoke.getagent(agentname) result = await loop.runinexecutor(None, agent.start, query) # no auth anywhere above return {"response": str(result)} @app.post(path) # default path "/agents" async def invokeagents(request: Request, querydata: AgentQuery = None): ... agent.start(query) ...
The auth dependency exists — it just isn't applied here createagentsapp() mounts the agentinvoke router into the same app: python cli/features/serve.py if getattr(agentinvoke, 'FASTAPIAVAILABLE', False) and hasattr(agentinvoke, 'router'): app.includerouter(agentinvoke.router) That router properly authenticates every sensitive route: python api/agentinvoke.py CALLSERVERTOKEN = os.getenv('CALLSERVERTOKEN') async def verifytoken(request, authorization=Header(None)) -> None: ... if token != CALLSERVERTOKEN: raise HTTPException(statuscode=401, detail="Unauthorized") @router.get("/api/v1/agents") async def listagents(: None = Depends(verifytoken)): ... # and register/unregister/info all use it So in the same process GET /api/v1/agents returns 401 without a token, while POST /agents/{agentname} returns 200. Note also that verifytoken reads the CALLSERVERTOKEN 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/{agentname} body {"query":"..."} with no / wrong / empty credentials Proof of Concept Built the real createagentsapp() and exercised it over HTTP via FastAPI TestClient. Only praisonaiagents.Agent is stubbed (.start() returns EXEC:<query>), so no real LLM/credentials. CALLSERVERTOKEN=expected-secret was set so the sibling /api/v1 router is genuinely armed — making the contrast explicit. Operator started with: --api-key expected-secret (CALLSERVERTOKEN also set) == Sibling /api/v1 route WITH Depends(verifytoken) == 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["apikey"] is set, build a shared auth dependency and attach it to every agent-invocation / state-changing route in createagentsapp() and createunifiedapp() (dependencies=[Depends(verify)]). - Reuse / unify with the existing verifytoken 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; verifytoken reads CALLSERVERTOKEN). - Use constant-time comparison (hmac.comparedigest); verifytoken currently uses !=. - Update discovery metadata from authmodes=["none"] to ["api-key","bearer"] for protected endpoints. - Regression tests next to tests/unit/testserveunified.py: createagentsapp({"apikey":"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 authdependency(expectedkey: str): async def verify(authorization: str | None = Header(None), xapikey: str | None = Header(None, alias="X-API-Key")): token = xapikey if authorization and authorization.startswith("Bearer "): token = authorization[7:] if not token or not hmac.comparedigest(token, expectedkey): raise HTTPException(statuscode=401, detail="Unauthorized") return Depends(verify)
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/PraisonAIto a version that resolves this vulnerability.Fixed in 4.6.58
Event History
Frequently Asked Questions
Which deployments are exposed?
Any PraisonAI deployment running `praisonai serve agents` that makes the direct agent routes reachable to network callers is exposed. This includes deployments started with `--api-key`, because that setting is not enforced on those routes.
Does an attacker need credentials or prior access?
No. A remote caller can invoke agents through `POST /agents` or `POST /agents/{agent_name}` without credentials. Missing credentials, invalid bearer tokens, invalid `X-API-Key` values, and empty bearer tokens all still reach `agent.start()`.
Are all agent API routes affected in the same way?
No. The direct n8n-compatible routes are unprotected, while `/api/v1/...` routes use the existing `verify_token` dependency. Exposure should be assessed specifically for the direct `/agents` routes.