GitHub Advisoryadvisory · PyPI

GHSA-7g3p-92qq-8wvh praisonaiagents: AgentServer declares auth_token but never enforces it on any route

HIGH Published 2026-08-25 Modified 2026-08-25 Official page ↗
Aliases
CVE-2026-55528
Ecosystem
PyPI
Packages
praisonaiagents
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N
Weaknesses
CWE-306, CWE-862
Reviewed
2026-08-25

Affected packages

• PyPI praisonaiagents: < 1.6.58 → fixed in 1.6.58

Description

Researcher: Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research Target: https://github.com/MervinPraison/PraisonAI --- Package: `praisonaiagents` on PyPI Affected version (empirically tested): 1.6.48 Component: `praisonaiagents.server.AgentServer` (the bundled HTTP / SSE server) --- TL;DR `AgentServer.ServerConfig` advertises an `auth_token: Optional[str] = None` field that operators set when they want to lock down the server. The `GET /info` endpoint even displays it back as `"auth_token": "***"` — strongly implying the value is wired into request authentication. It isn't. `AgentServer._create_app` never reads `auth_token`, never adds an authentication middleware, and never decorates any route with a dependency that checks it. Every route — `/info`, `/publish`, `/events`, `/health` — accepts unauthenticated requests regardless of whether `auth_token` is configured. The same package contains a *sibling* server, `praisonaiagents.ui.a2a.A2A`, written by the same developer, which implements the bearer-token pattern correctly via FastAPI's `Depends(_verify_auth)`. This rules out the "auth is not yet implemented; operators are expected to add it" reading: the developer knew the pattern but did not apply it to `AgentServer`. Root cause ``` Expected behavior when setting ServerConfig(auth_token="…"): "Only requests with a matching Authorization header will be accepted on /publish, /events, /info." Actual behavior (server/server.py, dist 1.6.48): • line 31 auth_token: Optional[str] = None # declared • line 39 "auth_token": "*" if self.auth_token else None # displayed • lines 122-204: no auth middleware, no Depends, no request.headers["Authorization"] read, no comparison to self.config.auth_token. Impact: The configuration knob is dead code from the route handlers' perspective. All routes always run. The operator has no signal that their auth_token was discarded — /info even confirms it was received by displaying "*". ``` Sibling proof — the same package gets it right elsewhere `praisonaiagents/ui/a2a/a2a.py`: ```python line 163 async def _verify_auth(authorization: Optional[str] = Header(None)): """Verify bearer token if auth_token is configured.""" if self.auth_token is None: return ... if len(parts) != 2 or parts[0].lower() != "bearer" \ or parts[1] != self.auth_token: raise HTTPException(status_code=401, ...) line 192 from fastapi import Depends _a2a_deps = [Depends(_verify_auth)] if self.auth_token else [] ``` That is the missing implementation. Porting it to `AgentServer` — either via Starlette `BaseHTTPMiddleware` or by switching to FastAPI and adding `Depends(_verify_auth)` to each route — closes the gap. Affected routes (empirically tested) | Route | Method | Accepts unauth requests? | Impact | |-------------|--------|--------------------------|------------------------------------------------------------------------| | `/info` | GET | Yes (200) | Leaks server config; confirms `auth_token` is set (`"*"`); reveals client count and CORS config. | | `/publish` | POST | Yes (200) | Anyone broadcasts arbitrary `{type, data}` to every subscribed agent. Event payload is whatever the attacker sends. | | `/events` | GET | Yes (200) | Anyone subscribes to the SSE stream and observes every event published by the server (and by any other anonymous attacker). | | `/health` | GET | Yes (200) | Leaks live SSE client count. | Impact The `/publish` and `/events` routes are the load-bearing ones. Together they let an unauthenticated network-adjacent attacker: 1. Inject control events into every agent process subscribed to the server. `AgentServer.broadcast(event_type, data)` puts the payload into every `SSEClient.queue`; any consumer dispatching on `event_type` will dispatch on the attacker-chosen type. Real deployments register handlers per event type via `AgentServer.on_event(...)`; an attacker who can guess (or enumerate via `/info` + inspection) a registered type can drive arbitrary handler invocations with attacker-chosen `data`. 2. Eavesdrop on the entire event bus by subscribing to `/events`. Whatever the legitimate publishers send is visible: agent observations, intermediate plans, tool inputs and outputs, user-supplied prompts that the operator believed were behind the `auth_token` wall. 3. Pivot via leaked config. `/info` is sufficient to enumerate `cors_origins` (helping plan cross-origin attacks if any of the listed origins are attacker-controlled) and to confirm that the target has bothered to set `auth_token`, signalling a high-value target. `SSEClient.queue` is a `queue.Queue` with no documented size cap; the event broadcaster does not check `max_connections` against publishers, only subscribers. An attacker can also flood `/publish` to fill every subscriber's queue, denying service to legitimate broadcasts (CWE-770). Not scored as the main impact above. Anchors praisonaiagents 1.6.48, file `praisonaiagents/server/server.py`: | Line | Symbol | What it shows | |-------|---------------------------------------------------------|---------------| | 31 | `auth_token: Optional[str] = None` | Declared. | | 39 | `"auth_token": "*" if self.auth_token else None` | Displayed (masked) in `/info`. | | 121 | `def _create_app(self):` | Route + middleware setup begins. | | 132 | `async def health(request):` | No auth check. | | 139 | `async def events(request):` | No auth check. | | 164 | `async def publish(request):` | No auth check. | | 182 | `async def info(request):` | No auth check. | | 190 | `routes = [Route("/health", …), Route("/events", …), Route("/publish", …), Route("/info", …)]` | Routes registered without `Depends`/middleware. | | 197 | `app = Starlette(routes=routes)` | App created. | | 200 | `app = CORSMiddleware(app, …)` | Only middleware added. | Source sha256 (1.6.48, `praisonaiagents/server/server.py`): `aac9497d515b5cb928070267b860b11ef38b537605e64659feef895b524ca7e4` (9,962 bytes). Sibling (same package, same field name, *enforced*): `praisonaiagents/ui/a2a/a2a.py:163-193`. Reproduction (empirical PoC) `poc/poc.py` starts `AgentServer` with `ServerConfig(auth_token="supersecret-not-actually-checked")` and then sends unauthenticated requests to each route. Run log (`poc/run-log.txt`): ``` [1] GET /info (no Authorization) -> HTTP 200 body: {"name":"PraisonAI Agent Server","version":"1.0.0","clients":0, "config":{"host":"127.0.0.1","port":18765,"cors_origins":[], "auth_token":"***","max_connections":100}} [2] POST /publish (no Authorization) -> HTTP 200 body: {"success":true,"clients":0} [3] GET /health (no Authorization) -> HTTP 200 [4] GET /events (no Authorization) -> HTTP 200 VULNERABLE: 4 unauthenticated routes VERDICT: VULNERABLE EXIT 0 ``` Suggested fix Make `AgentServer` reuse the A2A pattern. Smallest fix: ```python in _create_app, after `app = Starlette(routes=routes)`: if self.config.auth_token: from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import JSONResponse expected = "Bearer " + self.config.auth_token class _Auth(BaseHTTPMiddleware): async def dispatch(self, request, call_next): if request.url.path == "/health": # if /health should remain public return await…

Related

Alias · CVE (1)

  • CVE-2026-55528 PraisonAI is a multi-agent teams system. vulnerability

Weakness · CWE (2)

  • CWE-306 Missing Authentication for Critical Functionality weakness
  • CWE-862 Missing Authorization weakness

References