OFFLINE
Awaiting data
Security intelligence
MajorCritical vulnerability

CVE-2026-60091: PraisonAI: Jobs API is unauthenticated by default and allows attacker-controlled webhook SSRF

GitHub Advisories · officialPublished Oct 8, 2026Risk 37/100

## Summary PraisonAI's Async Jobs API enables its API-key middleware **only when `PRAISONAI_JOBS_API_KEY` is set**, so by default every endpoint is unauthenticated. An unauthenticated `POST /api/v1/runs` accepts an attacker-controlled `webhook_url`; on job completion the server POSTs the job payload to it via `httpx`. The `webhook_url` has an SSRF validator (`gethostbyname` + private-IP check), but it validates at request time while `httpx` re-resolves at connection time — a DNS-rebinding TOCTOU that reaches internal services. Runtime-confirmed as an **unauthenticated, blind** SSRF (the internal canary received the POST; the internal response is not returned to the attacker). Severity Medium. ## Details ### Affected component - Package: `praisonai` 4.6.63. Files: `src/praisonai/praisonai/jobs/server.py`, `jobs/router.py`, `jobs/executor.py`, `jobs/models.py`. ### Vulnerable code / root cause Path: `src/praisonai/praisonai/jobs/server.py` Function: `create_app` Snippet: ```python jobs_api_key = os.environ.get("PRAISONAI_JOBS_API_KEY") # ... if jobs_api_key: app.add_middleware(JobsAPIKeyMiddleware) # auth ONLY when env var is set ``` Issue: with the env var unset (default), no auth middleware is added → all endpoints unauthenticated. Default bind is `127.0.0.1`. Path: `src/praisonai/praisonai/jobs/router.py` Function: `submit_job` Snippet: ```python @router.post("", response_model=JobSubmitResponse, status_code=202) async def submit_job(request, response, body: JobSubmitRequest, ...): # no auth dependency; body.webhook_url is attacker-controlled job = Job(prompt=body.prompt, webhook_url=body.webhook_url, ...) await executor.submit(job) ``` Issue: attacker-controlled `webhook_url` flows into the job with no authentication on the endpoint. Path: `src/praisonai/praisonai/jobs/models.py` (validator) and `src/praisonai/praisonai/jobs/executor.py` (sink) Function: `validate_webhook_url` → `_send_webhook` Snippet: ```python # models.py validate_webhook_url (CHECK time) ip = socket.gethostbyname(hostname) if ipaddress.ip_address(ip).is_private or ...: raise ValueError("Webhook URL resolves to a private or restricted network address") # executor.py _send_webhook (CONNECT time, re-resolves, no pinning) async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post(job.webhook_url, json=payload, ...) ``` Issue: the validator resolves the hostname at validation time but does not pin the IP for the `httpx.post` connection → DNS rebinding (independent resolution at check vs connect) bypasses it and reaches internal services. ### Attack flow 1. Operator runs the Jobs API without `PRAISONAI_JOBS_API_KEY` (default → unauth). 2. Attacker `POST /api/v1/runs` with `webhook_url` = a rebinding domain. 3. The validator(s) see a public IP (pass); on completion `httpx.post` re-resolves to an internal IP and connects → SSRF to internal. ### Why existing protection is bypassed Auth is opt-in (only when the env var is set). The `webhook_url` validator resolves at check time but does not pin the IP for the connection → DNS-rebinding TOCTOU. (Correction to an earlier static note: the guard exists but is bypassable.) ### Security boundary Unauthenticated network peer → server-side request to internal services (Scope: Changed). Default bind `127.0.0.1` limits remote reach unless the operator binds non-loopback. ## Proof of Concept ### Environment Real Jobs API (`python -m praisonai.jobs.server`, no API key) in a local runtime (`127.0.0.1:18085`), resolver pointed at the controlled rebinding DNS, internal canary Docker-internal only. Runnable assets: `PraisonAI-Runtime-Repro\runtime-files\` (`docker-compose.jobs.yml`). ### Steps to reproduce 1. `PRAI-04-01-Jobs-Submit-NoAuth`: `POST /api/v1/runs {"prompt":"hello"}` → `202` (no auth). 2. `PRAI-04-02-Jobs-Webhook-SSRF-Blind`: `POST /api/v1/runs {"prompt":"hello","webhook_url":"http://rebind.lab:8081/secret"}` → `202`. ### Expected result Unauthenticated job submission should be rejected; the webhook SSRF guard should prevent reaching internal services regardless of DNS timing. ## Impact Unauthenticated job submission (LLM cost abuse); SSRF to internal services (blind, DNS-rebinding); exfiltration of the job result to an attacker-controlled webhook. ## Suggested remediation - Authenticate the Jobs API by default (auto-generate a token / fail closed when binding non-loopback), like the gateway. - For `webhook_url`: resolve once, reject private/loopback/CGNAT/metadata, then pin and connect to the validated IP; disable redirects.

Upgrade affected packages to a patched version: praisonai 4.6.78.

Vendor
Not specified
Product
praisonai
Exploitation
none known
Evidence
official
CVSS
7.2

This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.

Open primary source