Skip to content

Commit 341bd60

Browse files
committed
Add changelog, fix some issues
1 parent fa17de1 commit 341bd60

5 files changed

Lines changed: 74 additions & 20 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ htmlcov/
5757
/*.iml
5858
*.sublime-project
5959
*.sublime-workspace
60+
.cursor
61+
.cursorrules
6062

6163
# ignore ctags file
6264
tags

changelog/68448.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Quote cmd.exe /c payloads on Windows so compound commands (e.g. cd ... & dir) work with runas

salt/platform/win.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,19 +1339,29 @@ def CreateProcessWithLogonW(
13391339
return process_info
13401340

13411341

1342+
def _cmd_exe_cswitch_quoted_argument(payload: str) -> str:
1343+
"""
1344+
Wrap ``payload`` for use as the argument to ``cmd.exe``'s ``/c`` switch.
1345+
1346+
Doubles embedded double quotes per ``cmd.exe`` parsing rules so the entire
1347+
payload is one argument when passed through ``CreateProcess`` /
1348+
``CreateProcessWithTokenW`` command lines (so ``&``, ``|``, etc. are not
1349+
parsed at the outer process level).
1350+
"""
1351+
return '"' + payload.replace('"', '""') + '"'
1352+
1353+
13421354
def prepend_cmd(win_shell, cmd):
13431355
"""
13441356
Prep cmd when shell is cmd.exe. Always use a command string instead of a
13451357
list to satisfy both CreateProcess and CreateProcessWithToken.
13461358
1347-
cmd must be double-quoted to ensure proper handling of space characters.
1348-
The first opening quote and the closing quote are stripped automatically by
1349-
the Win32 API.
1359+
The user payload is wrapped in double quotes after ``/c`` so compound
1360+
commands (e.g. ``cd /d ... & dir``) and paths with spaces behave correctly
1361+
under ``CreateProcessWithTokenW``.
13501362
"""
13511363
if isinstance(cmd, (list, tuple)):
13521364
args = subprocess.list2cmdline(cmd)
13531365
else:
13541366
args = cmd
1355-
new_cmd = f"{win_shell} /c {args}"
1356-
1357-
return new_cmd
1367+
return f"{win_shell} /c {_cmd_exe_cswitch_quoted_argument(args)}"

tests/pytests/functional/utils/test_win_runas.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Test the win_runas util
33
"""
44

5+
import os
6+
import tempfile
57
from random import randint
68

79
import pytest
@@ -42,8 +44,8 @@ def int_user():
4244
"cmd, expected",
4345
[
4446
("hostname && whoami", "username"),
45-
("hostname && echo foo", "foo"),
46-
("hostname && python --version", "Python"),
47+
("hostname & echo foo", "foo"),
48+
("hostname & python --version", "Python"),
4749
],
4850
)
4951
def test_compound_runas(user, cmd, expected):
@@ -61,8 +63,8 @@ def test_compound_runas(user, cmd, expected):
6163
"cmd, expected",
6264
[
6365
("hostname && whoami", "username"),
64-
("hostname && echo foo", "foo"),
65-
("hostname && python --version", "Python"),
66+
("hostname & echo foo", "foo"),
67+
("hostname & python --version", "Python"),
6668
],
6769
)
6870
def test_compound_runas_unpriv(user, cmd, expected):
@@ -76,6 +78,44 @@ def test_compound_runas_unpriv(user, cmd, expected):
7678
assert expected in result["stdout"]
7779

7880

81+
def test_runas_cd_ampersand_dir(user):
82+
"""
83+
``cd /d ... & dir`` must run both parts in the same cmd /c line under runas
84+
(regression for CreateProcessWithTokenW command-line parsing).
85+
"""
86+
with tempfile.TemporaryDirectory() as tmpdir:
87+
marker = "salt_runas_cd_dir_marker.txt"
88+
marker_path = os.path.join(tmpdir, marker)
89+
with open(marker_path, "w", encoding="utf-8") as f:
90+
f.write("x")
91+
inner = f'cd /d "{tmpdir}" & dir /b'
92+
result = win_runas.runas(
93+
cmd=salt.platform.win.prepend_cmd("cmd", inner),
94+
username=user.username,
95+
password=user.password,
96+
)
97+
assert result["retcode"] == 0, result
98+
lines = [line.strip() for line in result["stdout"].splitlines() if line.strip()]
99+
assert marker in lines, (result["stdout"], lines)
100+
101+
102+
def test_runas_unpriv_cd_ampersand_dir(user):
103+
with tempfile.TemporaryDirectory() as tmpdir:
104+
marker = "salt_runas_cd_dir_marker_unpriv.txt"
105+
marker_path = os.path.join(tmpdir, marker)
106+
with open(marker_path, "w", encoding="utf-8") as f:
107+
f.write("x")
108+
inner = f'cd /d "{tmpdir}" & dir /b'
109+
result = win_runas.runas_unpriv(
110+
cmd=salt.platform.win.prepend_cmd("cmd", inner),
111+
username=user.username,
112+
password=user.password,
113+
)
114+
assert result["retcode"] == 0, result
115+
lines = [line.strip() for line in result["stdout"].splitlines() if line.strip()]
116+
assert marker in lines, (result["stdout"], lines)
117+
118+
79119
def test_runas_str_user(user):
80120
result = win_runas.runas(
81121
cmd="whoami", username=user.username, password=user.password

tests/pytests/unit/platform/test_win.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,33 @@
1414
@pytest.mark.parametrize(
1515
"command, expected",
1616
[
17-
("whoami", "cmd.exe /c whoami"),
18-
("cmd /c hostname", "cmd.exe /c cmd /c hostname"),
19-
("echo foo", "cmd.exe /c echo foo"),
20-
('cmd /c "echo foo"', 'cmd.exe /c cmd /c "echo foo"'),
21-
("icacls 'C:\\Program Files'", "cmd.exe /c icacls 'C:\\Program Files'"),
17+
("whoami", 'cmd.exe /c "whoami"'),
18+
("cmd /c hostname", 'cmd.exe /c "cmd /c hostname"'),
19+
("echo foo", 'cmd.exe /c "echo foo"'),
20+
('cmd /c "echo foo"', 'cmd.exe /c "cmd /c ""echo foo"""'),
21+
("icacls 'C:\\Program Files'", 'cmd.exe /c "icacls \'C:\\Program Files\'"'),
2222
(
2323
"icacls 'C:\\Program Files' && echo 1",
24-
"cmd.exe /c icacls 'C:\\Program Files' && echo 1",
24+
'cmd.exe /c "icacls \'C:\\Program Files\' && echo 1"',
2525
),
2626
(
2727
["secedit", "/export", "/cfg", "C:\\A Path\\with\\a\\space"],
28-
'cmd.exe /c secedit /export /cfg "C:\\A Path\\with\\a\\space"',
28+
'cmd.exe /c "secedit /export /cfg ""C:\\A Path\\with\\a\\space"""',
2929
),
3030
(
3131
["C:\\a space\\a space.bat", "foo foo", "bar bar"],
32-
'cmd.exe /c "C:\\a space\\a space.bat" "foo foo" "bar bar"',
32+
'cmd.exe /c """C:\\a space\\a space.bat"" ""foo foo"" ""bar bar"""',
3333
),
3434
(
3535
'''echo "&<>[]|{}^=;!'+,`~ "''',
36-
'''cmd.exe /c echo "&<>[]|{}^=;!'+,`~ "''',
36+
'cmd.exe /c "echo ""&<>[]|{}^=;!\'+,`~ """',
3737
),
3838
],
3939
)
4040
def test_prepend_cmd(command, expected):
4141
"""
42-
Test that the command is prepended with "cmd /c" and quoted
42+
Test that the command is prepended with ``cmd /c`` and the payload is quoted
43+
for ``CreateProcess`` command-line parsing.
4344
"""
4445
win_shell = "cmd.exe"
4546
result = win.prepend_cmd(win_shell, command)

0 commit comments

Comments
 (0)