From 116b29bdc29d2b5b5ca6cd936a0752fc1d3ed18e Mon Sep 17 00:00:00 2001 From: Reddraconi Date: Fri, 24 Apr 2026 21:09:26 -0500 Subject: [PATCH] fix(security): drop shell=True from subprocess invocations Removes `shell=True` from the three Windows-platform subprocess calls in qt/utils/file_opener.py and the ripgrep invocation in core/library/refresh.py. Each call now passes its arguments as a list and goes straight to execvp / CreateProcess, so filenames containing shell metacharacters (&, ^, %, (), $(...), backticks) are treated as plain data instead of being parsed by cmd.exe or /bin/sh. - "Show in Explorer" passes ["explorer", f"/select,{path}"]. - The Windows-start fallback passes ["cmd", "/c", "start", "", path]. - The default Windows open uses `os.startfile`. - ripgrep is invoked with its arguments as a list. Behavior should be unchanged for ordinary filenames. We passed 231 of 231 tests and ruff and mypy came back clean. Refs TagStudioDev/TagStudio#1339 --- src/tagstudio/core/library/refresh.py | 19 ++++++++----------- src/tagstudio/qt/utils/file_opener.py | 26 ++++++-------------------- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/src/tagstudio/core/library/refresh.py b/src/tagstudio/core/library/refresh.py index a6225d6af..da258f590 100644 --- a/src/tagstudio/core/library/refresh.py +++ b/src/tagstudio/core/library/refresh.py @@ -93,19 +93,16 @@ def __get_dir_list(self, library_dir: Path, ignore_patterns: list[str]) -> list[ pattern_file.write("\n".join(ignore_patterns)) result = silent_run( - " ".join( - [ - "rg", - "--files", - "--follow", - "--hidden", - "--ignore-file", - f'"{str(compiled_ignore_path)}"', - ] - ), + [ + "rg", + "--files", + "--follow", + "--hidden", + "--ignore-file", + str(compiled_ignore_path), + ], cwd=library_dir, capture_output=True, - shell=True, encoding="UTF-8", ) compiled_ignore_path.unlink() diff --git a/src/tagstudio/qt/utils/file_opener.py b/src/tagstudio/qt/utils/file_opener.py index e1b27c276..eacccda0a 100644 --- a/src/tagstudio/qt/utils/file_opener.py +++ b/src/tagstudio/qt/utils/file_opener.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio +import os import shutil import subprocess import sys @@ -40,39 +41,24 @@ def open_file(path: str | Path, file_manager: bool = False, windows_start_comman if sys.platform == "win32": normpath = str(Path(path).resolve()) if file_manager: - command_name = "explorer" - command_arg = f'/select,"{normpath}"' - - # For some reason, if the args are passed in a list, this will error when the - # path has spaces, even while surrounded in double quotes. + # /select, takes the path as a single comma-joined argument. silent_popen( - command_name + command_arg, - shell=True, + ["explorer", f"/select,{normpath}"], close_fds=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_BREAKAWAY_FROM_JOB, ) else: if windows_start_command: - command_name = "start" - # First parameter is for title, NOT filepath - command_args = ["", normpath] + # First "" is the start-command title slot, not the filepath. subprocess.Popen( - [command_name] + command_args, - shell=True, + ["cmd", "/c", "start", "", normpath], close_fds=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_BREAKAWAY_FROM_JOB, ) else: - command = f'"{normpath}"' - silent_popen( - command, - shell=True, - close_fds=True, - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP - | subprocess.CREATE_BREAKAWAY_FROM_JOB, - ) + os.startfile(normpath) else: if sys.platform == "darwin": command_name = "open"