From 61dcbb62ac0897b378363abb33804b8284b44786 Mon Sep 17 00:00:00 2001 From: baolingao Date: Fri, 19 Jun 2026 03:06:11 +0800 Subject: [PATCH] fix(cli): mitigate shell injection in quick_commands exec via shlex.split() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prefer shlex.split() + shell=False for user-defined quick commands, falling back to shell=True only when the command contains shell operators (pipes, redirects, chaining) that cannot be expressed as an argv list. This reduces the attack surface if config.yaml is compromised (CWE-78) while preserving backward compatibility for existing configurations that rely on shell features. Tested: - echo hello → shell=False, runs - echo hello | wc -c → shell=True fallback, runs - echo "unclosed → shlex.split() ValueError → shell=True fallback --- cli.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/cli.py b/cli.py index 52bfe6cdb0a93..b471679711b09 100644 --- a/cli.py +++ b/cli.py @@ -7777,16 +7777,35 @@ def process_command(self, command: str) -> bool: if base_cmd.lstrip("/") in quick_commands: qcmd = quick_commands[base_cmd.lstrip("/")] if qcmd.get("type") == "exec": + import shlex import subprocess exec_cmd = qcmd.get("command", "") if exec_cmd: try: - # shell=True is intentional: quick_commands are user-defined - # shell snippets from config.yaml — not agent/LLM controlled. - result = subprocess.run( - exec_cmd, shell=True, capture_output=True, - text=True, timeout=30 - ) + # Prefer argv-list execution to reduce shell injection + # surface if config.yaml is compromised (CWE-78). Fall + # back to shell=True only when the command uses shell + # features (pipes, redirects, chaining) that cannot be + # expressed as a plain argv list. + try: + cmd_parts = shlex.split(exec_cmd) + except ValueError: + use_shell = True + cmd_parts = exec_cmd + else: + shell_operators = {'|', ';', '&&', '||', '>', '<', '>>', '&', '$('} + use_shell = any(op in exec_cmd for op in shell_operators) + + if use_shell: + result = subprocess.run( + exec_cmd, shell=True, capture_output=True, + text=True, timeout=30 + ) + else: + result = subprocess.run( + cmd_parts, capture_output=True, + text=True, timeout=30 + ) output = result.stdout.strip() or result.stderr.strip() if output: self._console_print(_rich_text_from_ansi(output))