Логотип exploitDog
Консоль
Логотип exploitDog

exploitDog

github логотип

GHSA-r7v3-x45f-g7hp

Опубликовано: 25 авг. 2026
Источник: github
Github: Прошло ревью
CVSS3: 7.3

Описание

PraisonAI: [Auth Bypass] praisonai serve agents --api-key is silently ignored — agent-invocation routes (POST /agents, POST /agents/{agent_name}) run unauthenticated

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 modulepraisonai.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:

# 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:

# 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():

# 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:

# 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:

# 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:<query>), 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

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.
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)

Пакеты

Наименование

PraisonAI

pip
Затронутые версииВерсия исправления

< 4.6.58

4.6.58

EPSS

Процентиль: 18%
0.00258
Низкий

7.3 High

CVSS3

Дефекты

CWE-306

Связанные уязвимости

CVSS3: 7.3
nvd
22 дня назад

PraisonAI is a multi-agent teams system. Prior to praisonai 4.6.51, praisonai serve agents parses config["api_key"] but _create_agents_app() does not authenticate POST /agents or POST /agents/{agent_name}. Missing or incorrect bearer and X-API-Key values still reach agent execution. This issue is fixed in version 4.6.58.

EPSS

Процентиль: 18%
0.00258
Низкий

7.3 High

CVSS3

Дефекты

CWE-306