Skip to content

Commit c6b96a8

Browse files
authored
Add safeguards for constant subprocess commands (#420)
1 parent 92629a8 commit c6b96a8

File tree

3 files changed

+136
-53
lines changed

3 files changed

+136
-53
lines changed

integration_tests/test_process_sandbox.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,29 @@ class TestProcessSandbox(BaseIntegrationTest):
88
original_code = """
99
import subprocess
1010
11-
subprocess.run("echo 'hi'", shell=True)
12-
subprocess.run(["ls", "-l"])
11+
cmd = " ".join(["ls"])
12+
13+
subprocess.run(cmd, shell=True)
14+
subprocess.run([cmd, "-l"])
1315
14-
subprocess.call("echo 'hi'", shell=True)
15-
subprocess.call(["ls", "-l"])
16+
subprocess.call(cmd, shell=True)
17+
subprocess.call([cmd, "-l"])
1618
17-
subprocess.check_output(["ls", "-l"])
19+
subprocess.check_output([cmd, "-l"])
1820
21+
subprocess.run("ls -l", shell=True)
22+
1923
var = "hello"
2024
"""
2125
replacement_lines = [
2226
(2, """from security import safe_command\n\n"""),
23-
(3, """safe_command.run(subprocess.run, "echo 'hi'", shell=True)\n"""),
24-
(4, """safe_command.run(subprocess.run, ["ls", "-l"])\n"""),
25-
(6, """safe_command.run(subprocess.call, "echo 'hi'", shell=True)\n"""),
26-
(7, """safe_command.run(subprocess.call, ["ls", "-l"])\n"""),
27+
(5, """safe_command.run(subprocess.run, cmd, shell=True)\n"""),
28+
(6, """safe_command.run(subprocess.run, [cmd, "-l"])\n"""),
29+
(8, """safe_command.run(subprocess.call, cmd, shell=True)\n"""),
30+
(9, """safe_command.run(subprocess.call, [cmd, "-l"])\n"""),
2731
]
28-
expected_diff = '--- \n+++ \n@@ -1,10 +1,11 @@\n import subprocess\n+from security import safe_command\n \n-subprocess.run("echo \'hi\'", shell=True)\n-subprocess.run(["ls", "-l"])\n+safe_command.run(subprocess.run, "echo \'hi\'", shell=True)\n+safe_command.run(subprocess.run, ["ls", "-l"])\n \n-subprocess.call("echo \'hi\'", shell=True)\n-subprocess.call(["ls", "-l"])\n+safe_command.run(subprocess.call, "echo \'hi\'", shell=True)\n+safe_command.run(subprocess.call, ["ls", "-l"])\n \n subprocess.check_output(["ls", "-l"])\n \n'
29-
expected_line_change = "3"
32+
expected_diff = '--- \n+++ \n@@ -1,12 +1,13 @@\n import subprocess\n+from security import safe_command\n \n cmd = " ".join(["ls"])\n \n-subprocess.run(cmd, shell=True)\n-subprocess.run([cmd, "-l"])\n+safe_command.run(subprocess.run, cmd, shell=True)\n+safe_command.run(subprocess.run, [cmd, "-l"])\n \n-subprocess.call(cmd, shell=True)\n-subprocess.call([cmd, "-l"])\n+safe_command.run(subprocess.call, cmd, shell=True)\n+safe_command.run(subprocess.call, [cmd, "-l"])\n \n subprocess.check_output([cmd, "-l"])\n \n'
33+
expected_line_change = "5"
3034
num_changes = 4
3135
num_changed_files = 2
3236
change_description = ProcessSandbox.change_description

src/core_codemods/process_creation_sandbox.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,20 @@ class ProcessSandbox(SimpleCodemod):
2727
rules:
2828
- pattern-either:
2929
- patterns:
30-
- pattern: subprocess.run(...)
30+
- pattern: subprocess.$FUNC(...)
31+
- pattern-not: subprocess.$FUNC("...", ...)
32+
- pattern-not: subprocess.$FUNC(["...", ...], ...)
33+
- metavariable-pattern:
34+
metavariable: $FUNC
35+
patterns:
36+
- pattern-either:
37+
- pattern: run
38+
- pattern: call
39+
- pattern: Popen
3140
- pattern-inside: |
3241
import subprocess
3342
...
34-
- patterns:
35-
- pattern: subprocess.call(...)
36-
- pattern-inside: |
37-
import subprocess
38-
...
39-
- patterns:
40-
- pattern: subprocess.Popen(...)
41-
- pattern-inside: |
42-
import subprocess
43-
...
44-
"""
43+
"""
4544

4645
def on_result_found(self, original_node, updated_node):
4746
self.add_needed_import("security", "safe_command")

tests/codemods/test_process_creation_sandbox.py

Lines changed: 110 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ def test_import_subprocess(self, adds_dependency, tmpdir):
1717
input_code = """
1818
import subprocess
1919
20-
subprocess.run("echo 'hi'", shell=True)
21-
var = "hello"
20+
def foo(cmd):
21+
subprocess.run(cmd, shell=True)
22+
var = "hello"
2223
"""
2324
expected = """
2425
import subprocess
2526
from security import safe_command
2627
27-
safe_command.run(subprocess.run, "echo 'hi'", shell=True)
28-
var = "hello"
28+
def foo(cmd):
29+
safe_command.run(subprocess.run, cmd, shell=True)
30+
var = "hello"
2931
"""
3032
self.run_and_assert(tmpdir, input_code, expected)
3133
adds_dependency.assert_called_once_with(Security)
@@ -34,15 +36,17 @@ def test_import_alias(self, adds_dependency, tmpdir):
3436
input_code = """
3537
import subprocess as sub
3638
37-
sub.run("echo 'hi'", shell=True)
38-
var = "hello"
39+
def foo(cmd):
40+
sub.run(cmd, shell=True)
41+
var = "hello"
3942
"""
4043
expected = """
4144
import subprocess as sub
4245
from security import safe_command
4346
44-
safe_command.run(sub.run, "echo 'hi'", shell=True)
45-
var = "hello"
47+
def foo(cmd):
48+
safe_command.run(sub.run, cmd, shell=True)
49+
var = "hello"
4650
"""
4751
self.run_and_assert(tmpdir, input_code, expected)
4852
adds_dependency.assert_called_once_with(Security)
@@ -51,22 +55,25 @@ def test_from_subprocess(self, adds_dependency, tmpdir):
5155
input_code = """
5256
from subprocess import run
5357
54-
run("echo 'hi'", shell=True)
55-
var = "hello"
58+
def foo(cmd):
59+
run(cmd, shell=True)
60+
var = "hello"
5661
"""
5762
expected = """
5863
from subprocess import run
5964
from security import safe_command
6065
61-
safe_command.run(run, "echo 'hi'", shell=True)
62-
var = "hello"
66+
def foo(cmd):
67+
safe_command.run(run, cmd, shell=True)
68+
var = "hello"
6369
"""
6470
self.run_and_assert(tmpdir, input_code, expected)
6571
adds_dependency.assert_called_once_with(Security)
6672

6773
def test_subprocess_nameerror(self, _, tmpdir):
6874
input_code = """
69-
subprocess.run("echo 'hi'", shell=True)
75+
def foo(cmd):
76+
subprocess.run(cmd, shell=True)
7077
7178
import subprocess
7279
"""
@@ -80,32 +87,36 @@ def test_subprocess_nameerror(self, _, tmpdir):
8087
"""
8188
import subprocess
8289
import csv
83-
subprocess.run("echo 'hi'", shell=True)
84-
csv.excel
90+
def foo(cmd):
91+
subprocess.run(cmd, shell=True)
92+
csv.excel
8593
""",
8694
"""
8795
import subprocess
8896
import csv
8997
from security import safe_command
9098
91-
safe_command.run(subprocess.run, "echo 'hi'", shell=True)
92-
csv.excel
99+
def foo(cmd):
100+
safe_command.run(subprocess.run, cmd, shell=True)
101+
csv.excel
93102
""",
94103
),
95104
(
96105
"""
97106
import subprocess
98107
from csv import excel
99-
subprocess.run("echo 'hi'", shell=True)
100-
excel
108+
def foo(cmd):
109+
subprocess.run(cmd, shell=True)
110+
excel
101111
""",
102112
"""
103113
import subprocess
104114
from csv import excel
105115
from security import safe_command
106116
107-
safe_command.run(subprocess.run, "echo 'hi'", shell=True)
108-
excel
117+
def foo(cmd):
118+
safe_command.run(subprocess.run, cmd, shell=True)
119+
excel
109120
""",
110121
),
111122
],
@@ -123,38 +134,62 @@ def test_multifunctions(self, adds_dependency, tmpdir):
123134
input_code = """
124135
import subprocess
125136
126-
subprocess.run("echo 'hi'", shell=True)
127-
subprocess.check_output(["ls", "-l"])"""
137+
def foo(cmd, cmd2):
138+
subprocess.run(cmd, shell=True)
139+
subprocess.check_output([cmd2, "-l"])"""
128140

129141
expected = """
130142
import subprocess
131143
from security import safe_command
132144
133-
safe_command.run(subprocess.run, "echo 'hi'", shell=True)
134-
subprocess.check_output(["ls", "-l"])"""
145+
def foo(cmd, cmd2):
146+
safe_command.run(subprocess.run, cmd, shell=True)
147+
subprocess.check_output([cmd2, "-l"])"""
135148

136149
self.run_and_assert(tmpdir, input_code, expected)
137150
adds_dependency.assert_called_once_with(Security)
138151

152+
@pytest.mark.parametrize("command", ["run", "Popen"])
153+
def test_subprocess_imported_cmd(self, adds_dependency, tmpdir, command):
154+
input_code = f"""
155+
import subprocess
156+
from whatever import x
157+
158+
subprocess.{command}([x, "-l"])
159+
"""
160+
expected = f"""
161+
import subprocess
162+
from whatever import x
163+
from security import safe_command
164+
165+
safe_command.run(subprocess.{command}, [x, "-l"])
166+
"""
167+
self.run_and_assert(tmpdir, input_code, expected)
168+
adds_dependency.assert_called_once_with(Security)
169+
139170
def test_custom_run(self, _, tmpdir):
140171
input_code = """
141172
from app_funcs import run
142173
143-
run("echo 'hi'", shell=True)"""
174+
def foo(cmd):
175+
run(cmd, shell=True)
176+
"""
144177
expected = input_code
145178
self.run_and_assert(tmpdir, input_code, expected)
146179

147180
def test_subprocess_call(self, adds_dependency, tmpdir):
148181
input_code = """
149182
import subprocess
150183
151-
subprocess.call(["ls", "-l"])
184+
def foo(cmd):
185+
subprocess.call([cmd, "-l"])
152186
"""
153187
expected = """
154188
import subprocess
155189
from security import safe_command
156190
157-
safe_command.run(subprocess.call, ["ls", "-l"])
191+
def foo(cmd):
192+
safe_command.run(subprocess.call, [cmd, "-l"])
158193
"""
159194
self.run_and_assert(tmpdir, input_code, expected)
160195
adds_dependency.assert_called_once_with(Security)
@@ -163,13 +198,58 @@ def test_subprocess_popen(self, adds_dependency, tmpdir):
163198
input_code = """
164199
import subprocess
165200
166-
subprocess.Popen(["ls", "-l"])
201+
def foo(cmd):
202+
subprocess.Popen([cmd, "-l"])
167203
"""
168204
expected = """
169205
import subprocess
170206
from security import safe_command
171207
172-
safe_command.run(subprocess.Popen, ["ls", "-l"])
208+
def foo(cmd):
209+
safe_command.run(subprocess.Popen, [cmd, "-l"])
173210
"""
174211
self.run_and_assert(tmpdir, input_code, expected)
175212
adds_dependency.assert_called_once_with(Security)
213+
214+
def test_hardcoded_string(self, _, tmpdir):
215+
input_code = (
216+
expected
217+
) = """
218+
import subprocess
219+
220+
subprocess.Popen("ls -l", shell=True)
221+
"""
222+
self.run_and_assert(tmpdir, input_code, expected)
223+
224+
def test_hardcoded_string_propagation(self, _, tmpdir):
225+
input_code = (
226+
expected
227+
) = """
228+
import subprocess
229+
230+
cmd = "ls -l"
231+
subprocess.Popen(cmd, shell=True)
232+
"""
233+
self.run_and_assert(tmpdir, input_code, expected)
234+
235+
def test_hardcoded_array(self, _, tmpdir):
236+
input_code = (
237+
expected
238+
) = """
239+
import subprocess
240+
241+
subprocess.Popen(["ls", "-l"])
242+
"""
243+
self.run_and_assert(tmpdir, input_code, expected)
244+
245+
@pytest.mark.xfail(reason="Semgrep doesn't seem to support array propagation")
246+
def test_hardcoded_array_propagation(self, _, tmpdir):
247+
input_code = (
248+
expected
249+
) = """
250+
import subprocess
251+
252+
cmd = ["ls", "-l"]
253+
subprocess.Popen(cmd)
254+
"""
255+
self.run_and_assert(tmpdir, input_code, expected)

0 commit comments

Comments
 (0)