# PraisonAI: Jobs webhook SSRF protection bypass via DNS rebinding

PraisonAI Jobs API validates webhook_url only at parse time, so an attacker-controlled hostname can rebind from public IP to loopback and receive internal POSTs.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by: rexpository
- Disclosed: 2026-06-18
- CVE: CVE-2026-57114
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-rjvw-7vvw-549v
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-rjvw-7vvw-549v
- Tags: area:web domain:api domain:web impact:internal-access kind:research platform:ghsa sev:high tech:python tech:rest technique:incomplete-fix technique:source-review vuln:race-condition vuln:ssrf

**Target:** Async Jobs API webhook delivery (POST /api/v1/runs, _send_webhook)
**Root cause:** webhook_url is validated with urlparse/gethostbyname at Pydantic parse and Job construction, but _send_webhook passes the original hostname to httpx.AsyncClient.post() which re-resolves DNS with no send-time validation or IP pinning.
**Steps:**
1. Submit job with `webhook_url=http://rebind.test:<port>/hook` where rebind.test resolves to a public IP (e.g. 93.184.216.34) during validation.
2. JobSubmitRequest.validate_webhook_url() and Job.validate_webhook_url() accept it since the resolved address is public.
3. Job completes; JobExecutor._send_webhook() creates a fresh httpx.AsyncClient and POSTs to the original URL.
4. Attacker's DNS now returns 127.0.0.1 (or private/metadata IP); httpx connects to the internal target.
5. Internal server receives POST with webhook payload keys completed_at,duration_seconds,error,job_id,result,status.
**How found:** Noticed validation happens at parse/construct time but the actual network call re-resolves the hostname later — a TOCTOU gap. Confirmed by a control test showing direct loopback URLs are rejected, then monkeypatching gethostbyname (validation) vs getaddrinfo (send) to model rebinding and observing the loopback server hit.
**Exploitation:** Attacker needs job submission access (unauthenticated by default per docs). Control DNS for the webhook hostname, serve public A record during validation, flip to loopback/private/metadata IP before delivery. PraisonAI host then POSTs attacker-influenced job result payload to internal services; blind SSRF, port probing via timing/errors, and metadata interaction where method/network permit.
**Insight:** When a guard resolves a hostname but the HTTP client resolves it again, the guard is decorative — check whether validation and connection share a resolution step; if not, DNS rebinding defeats it. Also check whether the fix for a prior SSRF advisory only added parse-time checks.

## Proof of concept (copied exactly from the original report)

**Command to run the PoV**

```
env PYTHONPATH=src/praisonai python3 poc_jobs_webhook_dns_rebinding_ssrf.py
```

