From c2a695d4664fd8c322e793e9b27ef6940d01ad21 Mon Sep 17 00:00:00 2001 From: Aditya Bavadekar Date: Mon, 27 Oct 2025 13:33:24 +0530 Subject: [PATCH] fix: sanitize shell input - Replaced unsafe string concatenation with `shlex.join()` for POSIX systems, `mslex.join()` for Windows systems. - Add utility function `safe_join`. - Modified `pyproject.toml` to include `mslex` dependency for Windows environments. --- .../computer/computer/providers/lume_api.py | 10 +++++--- libs/python/computer/computer/utils.py | 25 +++++++++++++++++++ libs/python/computer/pyproject.toml | 5 ++-- uv.lock | 17 ++++++++++--- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/libs/python/computer/computer/providers/lume_api.py b/libs/python/computer/computer/providers/lume_api.py index d034e1df..1e3b3053 100644 --- a/libs/python/computer/computer/providers/lume_api.py +++ b/libs/python/computer/computer/providers/lume_api.py @@ -10,6 +10,8 @@ import subprocess import urllib.parse from typing import Any, Dict, List, Optional +from computer.utils import safe_join + # Setup logging logger = logging.getLogger(__name__) @@ -59,7 +61,7 @@ def lume_api_get( # --max-time: Maximum time for the whole operation (20 seconds) # -f: Fail silently (no output at all) on server errors # Add single quotes around URL to ensure special characters are handled correctly - cmd = ["curl", "--connect-timeout", "15", "--max-time", "20", "-s", "-f", f"'{api_url}'"] + cmd = ["curl", "--connect-timeout", "15", "--max-time", "20", "-s", "-f", api_url] # For logging and display, show the properly escaped URL display_cmd = ["curl", "--connect-timeout", "15", "--max-time", "20", "-s", "-f", api_url] @@ -71,7 +73,7 @@ def lume_api_get( # Execute the command - for execution we need to use shell=True to handle URLs with special characters try: # Use a single string with shell=True for proper URL handling - shell_cmd = " ".join(cmd) + shell_cmd = safe_join(cmd) result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True) # Handle curl exit codes @@ -514,7 +516,7 @@ def lume_api_delete( "-s", "-X", "DELETE", - f"'{api_url}'", + api_url, ] # For logging and display, show the properly escaped URL @@ -537,7 +539,7 @@ def lume_api_delete( # Execute the command - for execution we need to use shell=True to handle URLs with special characters try: # Use a single string with shell=True for proper URL handling - shell_cmd = " ".join(cmd) + shell_cmd = safe_join(cmd) result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True) # Handle curl exit codes diff --git a/libs/python/computer/computer/utils.py b/libs/python/computer/computer/utils.py index c2f04c48..43c7efcd 100644 --- a/libs/python/computer/computer/utils.py +++ b/libs/python/computer/computer/utils.py @@ -1,7 +1,10 @@ import base64 import io +import os +import shlex from typing import Any, Dict, Optional, Tuple +import mslex from PIL import Image, ImageDraw @@ -104,3 +107,25 @@ def parse_vm_info(vm_info: Dict[str, Any]) -> Optional[Dict[str, Any]]: """Parse VM info from pylume response.""" if not vm_info: return None + + +def safe_join(argv: list[str]) -> str: + """ + Return a platform-correct string that safely quotes `argv` for shell execution. + + - On POSIX: uses `shlex.join`. + - On Windows: uses `shlex.join`. + + Args: + argv: iterable of argument strings (will be coerced to str). + + Returns: + A safely quoted command-line string appropriate for the current platform that protects against + shell injection vulnerabilities. + """ + if os.name == "nt": + # On Windows, use mslex for proper quoting + return mslex.join(argv) + else: + # On POSIX systems, use shlex + return shlex.join(argv) diff --git a/libs/python/computer/pyproject.toml b/libs/python/computer/pyproject.toml index 110d24fb..1e4f49c4 100644 --- a/libs/python/computer/pyproject.toml +++ b/libs/python/computer/pyproject.toml @@ -16,7 +16,8 @@ dependencies = [ "websockets>=12.0", "aiohttp>=3.9.0", "cua-core>=0.1.0,<0.2.0", - "pydantic>=2.11.1" + "pydantic>=2.11.1", + "mslex>=1.3.0", ] requires-python = ">=3.12" @@ -47,4 +48,4 @@ source-includes = ["tests/", "README.md", "LICENSE"] [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] -python_files = "test_*.py" \ No newline at end of file +python_files = "test_*.py" diff --git a/uv.lock b/uv.lock index 9c93aea2..5c283383 100644 --- a/uv.lock +++ b/uv.lock @@ -861,7 +861,7 @@ wheels = [ [[package]] name = "cua-agent" -version = "0.4.35" +version = "0.4.37" source = { editable = "libs/python/agent" } dependencies = [ { name = "aiohttp" }, @@ -1015,11 +1015,12 @@ provides-extras = ["openai", "anthropic", "qwen", "omni", "uitars", "uitars-mlx" [[package]] name = "cua-computer" -version = "0.4.10" +version = "0.4.11" source = { editable = "libs/python/computer" } dependencies = [ { name = "aiohttp" }, { name = "cua-core" }, + { name = "mslex" }, { name = "pillow" }, { name = "pydantic" }, { name = "websocket-client" }, @@ -1046,6 +1047,7 @@ requires-dist = [ { name = "datasets", marker = "extra == 'ui'", specifier = ">=3.6.0" }, { name = "gradio", marker = "extra == 'all'", specifier = ">=5.23.3" }, { name = "gradio", marker = "extra == 'ui'", specifier = ">=5.23.3" }, + { name = "mslex", specifier = ">=1.3.0" }, { name = "pillow", specifier = ">=10.0.0" }, { name = "pydantic", specifier = ">=2.11.1" }, { name = "python-dotenv", marker = "extra == 'all'", specifier = ">=1.0.1" }, @@ -1057,7 +1059,7 @@ provides-extras = ["lume", "lumier", "ui", "all"] [[package]] name = "cua-computer-server" -version = "0.1.27" +version = "0.1.28" source = { editable = "libs/python/computer-server" } dependencies = [ { name = "aiohttp" }, @@ -3430,6 +3432,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "mslex" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/97/7022667073c99a0fe028f2e34b9bf76b49a611afd21b02527fbfd92d4cd5/mslex-1.3.0.tar.gz", hash = "sha256:641c887d1d3db610eee2af37a8e5abda3f70b3006cdfd2d0d29dc0d1ae28a85d", size = 11583, upload-time = "2024-10-16T13:16:18.523Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/f2/66bd65ca0139675a0d7b18f0bada6e12b51a984e41a76dbe44761bf1b3ee/mslex-1.3.0-py3-none-any.whl", hash = "sha256:c7074b347201b3466fc077c5692fbce9b5f62a63a51f537a53fbbd02eff2eea4", size = 7820, upload-time = "2024-10-16T13:16:17.566Z" }, +] + [[package]] name = "multidict" version = "6.7.0"