PraisonAI: Jobs API exposes agent-execution endpoints with no authentication
- When
- Where
- Global (internet)
- Category
- cyber_advisory · pip
# praisonai: Jobs API exposes agent-execution endpoints with no authentication **Researcher:** Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research **Target:** https://github.com/MervinPraison/PraisonAI --- **Package:** `praisonai` on PyPI **Affected version (empirically tested):** 4.6.48 **Components:** - `praisonai.jobs.server.create_app` — `praisonai/jobs/server.py` - `praisonai.jobs.router.create_router` — `praisonai/jobs/router.py` - Routes mounted at `/api/v1/runs/...` **Weakness:** CWE-306 Missing Authentication for Critical Function · CWE-862 Missing Authorization · CWE-94 Code Injection (via prompt / agent_yaml). --- ## TL;DR `praisonai` ships a standalone async-jobs HTTP server (`python -m praisonai.jobs.server --host=0.0.0.0 --port=8005`) whose job is to accept job submissions and run agents on the operator's behalf. Every endpoint under `/api/v1/runs` is **unauthenticated**. There is no `auth_token` field, no `Depends(verify_*)`, no middleware that inspects `Authorization` — the CORS middleware *lists* `Authorization` in `allow_headers` (the only signal in the whole module that the developer was aware authentication is a thing), but no route ever reads it. A network-reachable attacker can: 1. **Execute arbitrary agent code** — `POST /api/v1/runs` accepts `prompt`, `agent_yaml`, `agent_file`, `config`, `framework`. The job is queued and an executor invokes whichever framework (`praisonai` / `crewai` / `autogen`) the attacker picks, with whichever prompt and tool config the attacker supplies. The job runs in the operator's process — same environment variables, same filesystem, same credentials (OpenAI / Anthropic / Azure / Bedrock keys; tool integrations; on-disk YAML recipes). 2. **List and read every job system-wide** — `GET /api/v1/runs` lists all jobs; `GET /api/v1/runs/{job_id}/result` returns the full result of any completed job. Operator's prompts, the agent's chain-of-thought, tool inputs / outputs, retrieved documents — all readable to an anonymous client. 3. **Cancel or delete any job** — `POST /…/cancel` and `DELETE /…/{job_id}` accept arbitrary job IDs without any ownership / authorization check. 4. **Stream live SSE of any in-flight job** — `GET /…/{job_id}/stream` reads the executor's live progress for any job ID. The remote-RCE shape (1) is the load-bearing one. Even with `webhook_url` SSRF-guarded (and it is — the model validator at `jobs/models.py:42-65` rejects localhost / private IPs), the attacker needs no callback: SSE streaming returns the agent's output directly on the same connection. ## Root cause ``` Expected behavior when starting `praisonai.jobs.server`: "I'm running an HTTP API my application backend will call. The CORS middleware permits Authorization, so the server enforces it. Anonymous attackers cannot submit jobs." Actual behavior (praisonai 4.6.48): - server.py:59-152 create_app builds a FastAPI app, adds CORSMiddleware, includes the jobs router. NO auth middleware. NO global Depends. - router.py:43 @router.post("") submit_job(...) No Depends, no Authorization header read, no auth_token config field at all. - router.py:109,148,161,180,205,224 every other route: likewise, no auth on any of GET-list, GET-status, GET-result, POST-cancel, DELETE, GET-stream. - server.py:117 CORS allow_headers DOES include "Authorization" — the only token in the entire jobs/ subpackage that suggests the developer was thinking about auth. Impact: The API is intended to be production-ready (the CORS code at server.py:96-102 explicitly branches on `os.getenv("ENVIRONMENT") == "production"` to harden origins), yet ships with no authentication layer at all. Operators who bind the server to a network interface — including the suggested `--host=0.0.0.0` in the CLI parser — expose unauthenticated agent execution to anyone who can reach the port. ``` The same package gets auth right elsewhere (`praisonai/gateway/server.py` auto-generates an `auth_token` if none is configured and refuses to serve requests without it; `praisonai/endpoints/a2u_server.py:250-264` uses `hmac.compare_digest` on a Bearer token). The jobs API is the outlier. ## Empirically affected routes Verified by PoC against published `praisonai==4.6.48` (`/api/v1/runs/...` paths): | Method | Path | Unauth result | |----------|-------------------------------|--------------------------| | `POST` | `/api/v1/runs` | **HTTP 202 Accepted**, attacker job queued and executor invoked the framework | | `GET` | `/api/v1/runs` | **HTTP 200**, lists every job in the store | | `GET` | `/api/v1/runs/{job_id}` | **HTTP 200**, returns status of any job | | `GET` | `/api/v1/runs/{job_id}/result`| (untested; same router, no auth) | | `POST` | `/api/v1/runs/{job_id}/cancel`| **HTTP 200 / 409** (processed) | | `DELETE` | `/api/v1/runs/{job_id}` | **HTTP 204 No Content** (deleted) | | `GET` | `/api/v1/runs/{job_id}/stream`| (untested; SSE; same router, no auth) | PoC run log excerpt (`poc/run-log.txt`): ``` [1] POST /api/v1/runs (no Authorization) -> HTTP 202 body: {"job_id":"run_90f21c98b82a","status":"queued",...} [01:15:44] executor.py:201 ERROR Job failed: run_90f21c98b82a - OPENAI_API_KEY environment variable is required ... ``` The executor's error confirms the prompt reached the framework's LLM-invocation step. Had the operator set `OPENAI_API_KEY`, the attacker prompt would have executed. ## Impact details ### 1. Remote code execution via agent invocation `JobSubmitRequest.framework` accepts `"praisonai"`, `"crewai"`, or `"autogen"`. Each framework can be configured (via the YAML / config the attacker sends) to use arbitrary tools. praisonai's tool loaders (`praisonai/agents_generator.py` `load_tools_from_module*`) have a documented history of arbitrary-import (CVE-2026-40287 and its fix-of-fix CVE-2026-44334). In practice the operator's installation may or may not expose these sinks; either way the attacker controls the prompt, which the LLM will execute with whatever tools the operator wired (including shell, filesystem, browser, …). The job executor runs in-process under the operator's service account, with full access to environment variables (LLM API keys, tool tokens) and to anything `praisonai`'s tools normally touch. ### 2. Cross-tenant data read A single-process deployment uses an `InMemoryJobStore` that is flat — no `user_id` / `tenant_id` / `workspace_id` partition. Any client that knows or guesses a job ID can read it. Worse, the list endpoint (`GET /api/v1/runs`) returns every job, so guessing isn't even necessary. Sensitive content in the result includes the attacker's input (harmless) but also any *legitimate* user's input that the operator's backend submitted — and the agent's full output, which may contain data the agent retrieved from the operator's databases or APIs. ### 3. Denial of service via job deletion / cancellation `DELETE` and `cancel` accept any job ID. An attacker who polls the list endpoint can enumerate IDs and cancel-then-delete every job in flight, breaking the operator's backend's polling-for-completion flow. ### 4. webhook_url SSRF — defended To the developer's credit, `JobSubmitRequest.webhook_url` is validated against localhost / private / link-local / multicast IPs at submission time (`jobs/models.py:42-65`). This blocks the naive "submit a job whose webhook posts to AWS IMDS" attack. **Honest yield:** this is properly guarded. ## Anchors praisonai 4.6.48, source file `praisonai/jobs/server.py` (sha256 `10b5de
Sources
- GitHub Advisory Database ↗ · first seen 2026-06-18 13:57 UTC
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.