Skip to content

Commit 0313db6

Browse files
claudemldangelo
andcommitted
refactor: use cmd.exe on Windows instead of shell=True
Improves security and robustness: - Always use shell=False (more secure) - On Windows, explicitly call 'cmd /c' to execute .cmd files - Simpler type annotations (list[str] vs Union) - Consistent use of paths from shutil.which() - Follows Windows best practices for subprocess execution Co-Authored-By: Michael D'Angelo <michael@promptfoo.dev>
1 parent e92acdd commit 0313db6

File tree

1 file changed

+12
-29
lines changed

1 file changed

+12
-29
lines changed

src/promptfoo/cli.py

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import shutil
1111
import subprocess
1212
import sys
13-
from typing import NoReturn, Union
13+
from typing import NoReturn
1414

1515

1616
def check_node_installed() -> bool:
@@ -53,23 +53,11 @@ def main() -> NoReturn:
5353
# Try to find a globally installed promptfoo first
5454
promptfoo_path = shutil.which("promptfoo")
5555

56-
# Build the command
57-
cmd: Union[str, list[str]]
58-
use_shell: bool
59-
56+
# Build the command list (always use shell=False for security)
57+
# On Windows, we use cmd.exe to execute .cmd files properly
6058
if promptfoo_path:
6159
# Use the globally installed promptfoo
62-
if is_windows:
63-
# On Windows, build command as string for shell=True to handle .cmd files
64-
import shlex
65-
66-
args = ["promptfoo"] + sys.argv[1:]
67-
cmd = " ".join(shlex.quote(arg) for arg in args)
68-
use_shell = True
69-
else:
70-
# On Unix, use list format with shell=False (more secure)
71-
cmd = [promptfoo_path] + sys.argv[1:]
72-
use_shell = False
60+
cmd = ["cmd", "/c", promptfoo_path] + sys.argv[1:] if is_windows else [promptfoo_path] + sys.argv[1:]
7361
else:
7462
# Fall back to npx promptfoo@latest
7563
npx_path = shutil.which("npx")
@@ -78,25 +66,20 @@ def main() -> NoReturn:
7866
print("Please install promptfoo globally: npm install -g promptfoo", file=sys.stderr)
7967
sys.exit(1)
8068

81-
if is_windows:
82-
# On Windows, build command as string for shell=True to handle npx.cmd
83-
import shlex
84-
85-
args = ["npx", "--yes", "promptfoo@latest"] + sys.argv[1:]
86-
cmd = " ".join(shlex.quote(arg) for arg in args)
87-
use_shell = True
88-
else:
89-
# On Unix, use list format with shell=False (more secure)
90-
cmd = [npx_path, "--yes", "promptfoo@latest"] + sys.argv[1:]
91-
use_shell = False
69+
cmd = (
70+
["cmd", "/c", npx_path, "--yes", "promptfoo@latest"] + sys.argv[1:]
71+
if is_windows
72+
else [npx_path, "--yes", "promptfoo@latest"] + sys.argv[1:]
73+
)
9274

9375
try:
94-
# Execute the command and pass through stdio
76+
# Execute the command with shell=False (more secure)
77+
# Pass through stdio so the user interacts directly with promptfoo
9578
result = subprocess.run(
9679
cmd,
9780
env=os.environ.copy(),
9881
check=False, # Don't raise exception on non-zero exit
99-
shell=use_shell,
82+
shell=False, # Always False - more secure
10083
)
10184
sys.exit(result.returncode)
10285
except KeyboardInterrupt:

0 commit comments

Comments
 (0)