GHSA-VG6P-V9VM-6FGJ

Vulnerability from github – Published: 2026-08-25 14:43 – Updated: 2026-08-25 14:43
VLAI
Summary
praisonaiagents vulnerable to SSRF in web_crawl tool via redirect-following and DNS rebinding (validate-then-fetch gap)
Details

The web_crawl tool performs its SSRF check only on the initial URL: it resolves the hostname once with socket.gethostbyname and rejects private/loopback/link-local results. It then passes the URL to a fetcher that uses httpx.Client(follow_redirects=True) - or urllib.request.urlopen when httpx is absent, which also follows redirects - and re-resolves the hostname at connect time, with no further validation. This validate-here/fetch-there gap is bypassable two independent ways: HTTP redirects and DNS rebinding.

Affected code: src/praisonai-agents/praisonaiagents/tools/web_crawl_tools.py - Single-shot validation (lines 229-238): if os.environ.get("ALLOW_LOCAL_CRAWL") != "true": ip_str = socket.gethostbyname(hostname) # resolved ONCE, at validation time ip = ipaddress.ip_address(ip_str) if ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_multicast or ip.is_unspecified: continue # rejected url_list.append(u) - Vulnerable fetch (_crawl_with_httpx, lines 142 / 149): follows redirects, re-resolves DNS, no re-check: with httpx.Client(follow_redirects=True, timeout=30.0) as client: response = client.get(url) # fallback: urllib.request.urlopen(url, timeout=30) (also follows redirects by default) - web_crawl / crawl_web are registered tools (tools/init.py:156-157); httpx is the default fallback provider (dispatch at web_crawl_tools.py:269).

The two bypasses: 1) Redirect: validation approves an attacker domain resolving to a public IP; attacker server replies 302 Location: http://169.254.169.254/... (or any internal host); the fetcher follows it unchecked. 2) DNS rebinding (TOCTOU): validator's gethostbyname and fetcher's connect-time resolution are independent; a low-TTL attacker domain answers public to the validator and private/loopback to fetch.

Impact: An agent with web_crawl - driven by direct input or indirect prompt injection - can be made to read internal-only HTTP services and cloud instance-metadata endpoints (e.g. IAM credentials), with the response body returned in the tool output. Scope is Changed because the request pivots into the internal network.

Proof of concept: A PoC drives the real web_crawl() (httpx absent -> genuine urllib fallback). It runs a loopback "internal metadata" service and a loopback attacker redirector, substituting DNS only to stand in for "attacker owns a public domain" / offline routing - the redirect-following and connect-time re-resolution are the repo's own behavior. Observed: CONTROL: web_crawl("http://127.0.0.1:.../meta-data/") -> blocked (validator works) PoC 1A (redirect): attacker.example approved (public); 302 -> loopback metadata -> result.content leaks {"AccessKeyId":"ASIA_FAKE_STOLEN_CREDENTIAL_..."} PoC 1B (rebinding): gethostbyname(rebind.example)->public (allowed); connect->127.0.0.1 -> same secret leaked The control proves the validator blocks a direct loopback request, so the bypasses are genuine.

Remediation: Resolve the hostname once, validate that IP, and connect to that exact validated IP (pin it) rather than re-resolving. Disable redirect following (follow_redirects=False; for urllib use a redirect handler that re-validates), or re-validate every redirect hop's resolved IP. Apply the deny check to both the validator and the actual socket target. file_tools.py:364 already uses follow_redirects=False and is the correct pattern to propagate.

Distinct from prior advisories: The accepted SSRF advisories concern host-string parsing in different code — alternate loopback encodings in spider_tools (GHSA-5c6w-wwfq-7qqm) and the CLI @url feature (GHSA-5cxw-77wg-jrf3). This is in the web_crawl tool, which neither advisory names, and the mechanisms (redirect-following and DNS rebinding) differ categorically from host-string encoding; the spider_tools _host_is_blocked hardening does not apply to this tool.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "praisonaiagents"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.6.58"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55524"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367",
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-25T14:43:19Z",
    "nvd_published_at": "2026-08-05T20:17:10Z",
    "severity": "HIGH"
  },
  "details": "The web_crawl tool performs its SSRF check only on the initial URL: it resolves the hostname once\nwith socket.gethostbyname and rejects private/loopback/link-local results. It then passes the URL to\na fetcher that uses httpx.Client(follow_redirects=True) - or urllib.request.urlopen when httpx is\nabsent, which also follows redirects - and re-resolves the hostname at connect time, with no further\nvalidation. This validate-here/fetch-there gap is bypassable two independent ways: HTTP redirects and\nDNS rebinding.\n\nAffected code: src/praisonai-agents/praisonaiagents/tools/web_crawl_tools.py\n- Single-shot validation (lines 229-238):\n    if os.environ.get(\"ALLOW_LOCAL_CRAWL\") != \"true\":\n        ip_str = socket.gethostbyname(hostname)            # resolved ONCE, at validation time\n        ip = ipaddress.ip_address(ip_str)\n        if ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_multicast or ip.is_unspecified:\n            continue                                       # rejected\n    url_list.append(u)\n- Vulnerable fetch (_crawl_with_httpx, lines 142 / 149): follows redirects, re-resolves DNS, no re-check:\n    with httpx.Client(follow_redirects=True, timeout=30.0) as client: response = client.get(url)\n    # fallback: urllib.request.urlopen(url, timeout=30)  (also follows redirects by default)\n- web_crawl / crawl_web are registered tools (tools/__init__.py:156-157); httpx is the default fallback\n  provider (dispatch at web_crawl_tools.py:269).\n\nThe two bypasses:\n1) Redirect: validation approves an attacker domain resolving to a public IP; attacker server replies\n   302 Location: http://169.254.169.254/... (or any internal host); the fetcher follows it unchecked.\n2) DNS rebinding (TOCTOU): validator\u0027s gethostbyname and fetcher\u0027s connect-time resolution are\n   independent; a low-TTL attacker domain answers public to the validator and private/loopback to fetch.\n\nImpact:\nAn agent with web_crawl - driven by direct input or indirect prompt injection - can be made to read\ninternal-only HTTP services and cloud instance-metadata endpoints (e.g. IAM credentials), with the\nresponse body returned in the tool output. Scope is Changed because the request pivots into the\ninternal network.\n\nProof of concept:\nA PoC drives the real web_crawl() (httpx absent -\u003e genuine urllib fallback). It runs a loopback\n\"internal metadata\" service and a loopback attacker redirector, substituting DNS only to stand in\nfor \"attacker owns a public domain\" / offline routing - the redirect-following and connect-time\nre-resolution are the repo\u0027s own behavior. Observed:\n  CONTROL: web_crawl(\"http://127.0.0.1:.../meta-data/\")  -\u003e blocked (validator works)\n  PoC 1A (redirect):  attacker.example approved (public); 302 -\u003e loopback metadata\n                      -\u003e result.content leaks {\"AccessKeyId\":\"ASIA_FAKE_STOLEN_CREDENTIAL_...\"}\n  PoC 1B (rebinding): gethostbyname(rebind.example)-\u003epublic (allowed); connect-\u003e127.0.0.1\n                      -\u003e same secret leaked\nThe control proves the validator blocks a direct loopback request, so the bypasses are genuine.\n\nRemediation:\nResolve the hostname once, validate that IP, and connect to that exact validated IP (pin it) rather\nthan re-resolving. Disable redirect following (follow_redirects=False; for urllib use a redirect\nhandler that re-validates), or re-validate every redirect hop\u0027s resolved IP. Apply the deny check to\nboth the validator and the actual socket target. file_tools.py:364 already uses follow_redirects=False\nand is the correct pattern to propagate.\n\nDistinct from prior advisories:\nThe accepted SSRF advisories concern host-string parsing in different code \u2014 alternate loopback\nencodings in spider_tools (GHSA-5c6w-wwfq-7qqm) and the CLI @url feature (GHSA-5cxw-77wg-jrf3). This\nis in the web_crawl tool, which neither advisory names, and the mechanisms (redirect-following and DNS\nrebinding) differ categorically from host-string encoding; the spider_tools _host_is_blocked hardening\ndoes not apply to this tool.",
  "id": "GHSA-vg6p-v9vm-6fgj",
  "modified": "2026-08-25T14:43:19Z",
  "published": "2026-08-25T14:43:19Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-vg6p-v9vm-6fgj"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55524"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/releases/tag/v4.6.58"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "praisonaiagents vulnerable to SSRF in web_crawl tool via redirect-following and DNS rebinding (validate-then-fetch gap)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…