GHSA-R7V3-X45F-G7HP

Vulnerability from github – Published: 2026-08-25 14:56 – Updated: 2026-08-25 14:56
VLAI
Summary
PraisonAI: [Auth Bypass] `praisonai serve agents --api-key` is silently ignored — agent-invocation routes (`POST /agents`, `POST /agents/{agent_name}`) run unauthenticated
Details

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)
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "PraisonAI"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.6.58"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55538"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-306"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-25T14:56:59Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\n`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.\n \nAs 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()`.\n \nThe failure is made sharper by the fact that a **working auth dependency already exists in the same module** \u2014 `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.\n\n## Technical Detail\n \n### Source-to-sink trace\n \n**1. CLI advertises and forwards `--api-key`:**\n \n```python\n# cli/commands/serve.py\n@app.command(\"agents\")\ndef serve_agents(..., api_key: Optional[str] = typer.Option(None, \"--api-key\", help=\"API key for authentication\")):\n    ...\n    if api_key:\n        args.extend([\"--api-key\", api_key])\n    exit_code = handle_serve_command(args)\n```\n \n**2. `cmd_agents()` parses `api_key` into the spec \u2014 and that is the last time it is touched:**\n \n```python\n# cli/features/serve.py \u2014 cmd_agents()\nspec = { ..., \"api_key\": {\"default\": None} }\nparsed = self._parse_args(args, spec)\napp = self._create_agents_app(parsed)\n```\n \nA 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.\n \n**3. `_create_agents_app()` imports `FastAPI, HTTPException, Request` \u2014 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`.\n \n**4. Sink \u2014 unauthenticated request reaches `agent.start()`:**\n \n```python\n# cli/features/serve.py\n@app.post(\"/agents/{agent_name}\")          # n8n compatibility route\nasync def invoke_single_agent(agent_name: str, request: Request):\n    body = await request.json()\n    query = body.get(\"query\", \"\") or body.get(\"message\", \"\")\n    ...\n    agent = agent_invoke.get_agent(agent_name)\n    result = await loop.run_in_executor(None, agent.start, query)   # no auth anywhere above\n    return {\"response\": str(result)}\n \n@app.post(path)                            # default path \"/agents\"\nasync def invoke_agents(request: Request, query_data: AgentQuery = None):\n    ... agent.start(query) ...\n```\n\n### The auth dependency exists \u2014 it just isn\u0027t applied here\n \n`_create_agents_app()` mounts the `agent_invoke` router into the same app:\n \n```python\n# cli/features/serve.py\nif getattr(agent_invoke, \u0027FASTAPI_AVAILABLE\u0027, False) and hasattr(agent_invoke, \u0027router\u0027):\n    app.include_router(agent_invoke.router)\n```\n \nThat router properly authenticates every sensitive route:\n \n```python\n# api/agent_invoke.py\nCALL_SERVER_TOKEN = os.getenv(\u0027CALL_SERVER_TOKEN\u0027)\nasync def verify_token(request, authorization=Header(None)) -\u003e None:\n    ...\n    if token != CALL_SERVER_TOKEN:\n        raise HTTPException(status_code=401, detail=\"Unauthorized\")\n \n@router.get(\"/api/v1/agents\")\nasync def list_agents(_: None = Depends(verify_token)): ...   # and register/unregister/info all use it\n```\n \nSo 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 \u2014 **not** the CLI `--api-key` \u2014 so the CLI option feeds no auth path at all.\n \n### Trigger conditions\n \n```\npraisonai serve agents --file agents.yaml --host 0.0.0.0 --port 8765 --api-key expected-secret\nPOST /agents/{agent_name}   body {\"query\":\"...\"}   with no / wrong / empty credentials\n```\n \n## Proof of Concept\n \nBuilt the **real** `_create_agents_app()` and exercised it over HTTP via FastAPI `TestClient`. Only `praisonaiagents.Agent` is stubbed (`.start()` returns `EXEC:\u003cquery\u003e`), so no real LLM/credentials. `CALL_SERVER_TOKEN=expected-secret` was set so the sibling `/api/v1` router is genuinely armed \u2014 making the contrast explicit.\n \n```\nOperator started with: --api-key expected-secret  (CALL_SERVER_TOKEN also set)\n \n== Sibling /api/v1 route WITH Depends(verify_token) ==\n  GET /api/v1/agents [no creds    ] -\u003e HTTP 401\n  GET /api/v1/agents [wrong bearer] -\u003e HTTP 401\n \n== Direct agent-invocation route (the bug) ==\n  POST /agents/owned [no creds      ] -\u003e HTTP 200  {\u0027response\u0027: \u0027EXEC:hello\u0027}\n  POST /agents/owned [wrong bearer  ] -\u003e HTTP 200  {\u0027response\u0027: \u0027EXEC:hello\u0027}\n  POST /agents/owned [wrong x-api-key] -\u003e HTTP 200  {\u0027response\u0027: \u0027EXEC:hello\u0027}\n  POST /agents/owned [empty bearer  ] -\u003e HTTP 200  {\u0027response\u0027: \u0027EXEC:hello\u0027}\n  POST /agents       [no creds      ] -\u003e HTTP 200  {\u0027response\u0027: \u0027EXEC:hi\u0027}\n```\n \nThe auth mechanism works for `/api/v1` (401) and is entirely absent on the direct `/agents` routes (200), despite `--api-key` being configured.\n \n### Equivalent HTTP trigger in a fully installed environment\n \n```bash\npraisonai serve agents --file agents.yaml --host 0.0.0.0 --port 8765 --api-key expected-secret\ncurl -sS -X POST http://TARGET:8765/agents/owned \\\n  -H \u0027Content-Type: application/json\u0027 --data-binary \u0027{\"query\":\"hello\"}\u0027\n# -\u003e 200 {\"response\":\"...\"}  (expected: 401 Unauthorized)\n```\n\n## Impact\n \n- **Direct primitive**: unauthenticated agent invocation despite a configured API key.\n- **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.\n- **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.\n\n## Suggested Mitigation\n \n- 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)]`).\n- 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`).\n- Use constant-time comparison (`hmac.compare_digest`); `verify_token` currently uses `!=`.\n- Update discovery metadata from `auth_modes=[\"none\"]` to `[\"api-key\",\"bearer\"]` for protected endpoints.\n- Regression tests next to `tests/unit/test_serve_unified.py`: `_create_agents_app({\"api_key\":\"secret\",...})` \u2192 `POST /agents/{name}` with no creds / wrong `Authorization` / wrong `X-API-Key` returns `401`; correct key succeeds.\n```python\nimport hmac\nfrom fastapi import Header, HTTPException, Depends\n \ndef _auth_dependency(expected_key: str):\n    async def verify(authorization: str | None = Header(None),\n                     x_api_key: str | None = Header(None, alias=\"X-API-Key\")):\n        token = x_api_key\n        if authorization and authorization.startswith(\"Bearer \"):\n            token = authorization[7:]\n        if not token or not hmac.compare_digest(token, expected_key):\n            raise HTTPException(status_code=401, detail=\"Unauthorized\")\n    return Depends(verify)\n```",
  "id": "GHSA-r7v3-x45f-g7hp",
  "modified": "2026-08-25T14:56:59Z",
  "published": "2026-08-25T14:56:59Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-r7v3-x45f-g7hp"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/releases/tag/v4.6.58"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "PraisonAI: [Auth Bypass] `praisonai serve agents --api-key` is silently ignored \u2014 agent-invocation routes (`POST /agents`, `POST /agents/{agent_name}`) run unauthenticated"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…