{"id":"CVE-2026-55524","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-55524","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\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's gethostbyname and fetcher's 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 -> 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's own behavior. Observed:\n  CONTROL: web_crawl(\"http://127.0.0.1:.../meta-data/\")  -> blocked (validator works)\n  PoC 1A (redirect):  attacker.example approved (public); 302 -> loopback metadata\n                      -> result.content leaks {\"AccessKeyId\":\"ASIA_FAKE_STOLEN_CREDENTIAL_...\"}\n  PoC 1B (rebinding): gethostbyname(rebind.example)->public (allowed); connect->127.0.0.1\n                      -> 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's 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 — 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.","published":"2026-08-25T14:43:19Z","modified":"2026-08-25T23:50:07.053721444Z","cvss":{"score":7.5,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"praisonaiagents","fixedVersion":"1.6.58"}],"fix":{"url":"https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1","label":"MervinPraison/PraisonAI@2f9677a"},"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"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-25T23:50:07.053721444Z"}}