# PraisonAI: Jobs API is unauthenticated by default and allows attacker-controlled webhook SSRF

PraisonAI Jobs API ships unauthenticated by default and its webhook_url SSRF guard is bypassable via DNS rebinding, letting attackers reach internal services blindly.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by (vendor credit): dinhvaren
- Found by: dinhvaren
- Disclosed: 2026-10-08
- CVE: CVE-2026-60091
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-4w49-gwv8-fpjg
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4w49-gwv8-fpjg
- Tags: area:ai domain:ai domain:web impact:internal-access kind:research platform:ghsa sev:high tech:python technique:race-window technique:source-review vuln:auth-bypass vuln:race-condition vuln:ssrf

**Target:** Async Jobs API (POST /api/v1/runs) and webhook delivery via httpx
**Root cause:** API-key middleware is only added when PRAISONAI_JOBS_API_KEY is set, so endpoints are unauthenticated by default. The webhook_url validator resolves the hostname at check time but httpx re-resolves at connect time without IP pinning, enabling DNS-rebinding TOCTOU.
**Steps:**
1. Run Jobs API without PRAISONAI_JOBS_API_KEY (`python -m praisonai.jobs.server`).
2. `POST /api/v1/runs {"prompt":"hello"}` → `202` with no auth.
3. `POST /api/v1/runs {"prompt":"hello","webhook_url":"http://rebind.lab:8081/secret"}` → `202`.
4. Validator's `gethostbyname` sees a public IP and passes; on job completion `httpx.post` re-resolves to an internal IP and connects.
**How found:** Code review of server.py create_app showed auth middleware gated on an env var; reading models.py validator vs executor.py sink revealed independent DNS resolution at check vs connect.
**Exploitation:** Attacker submits a job with a rebinding-domain webhook_url; the check-time resolution returns a public IP, then the connect-time resolution points to an internal host, so the server POSTs the job payload to internal services. Blind: internal response not returned, but job result is exfiltrated to the attacker webhook.
**Escalation:** Default bind 127.0.0.1 limits remote reach unless operator binds non-loopback; unauthenticated submission also enables LLM cost abuse.
**Insight:** When a URL validator calls gethostbyname and the HTTP client resolves separately, assume rebinding works — check whether the validated IP is pinned into the connection; also grep for auth middleware added conditionally on env vars.

