Skip to content

Commit c647ec2

Browse files
authored
Semgrep subprocess shell False codemod (#706)
* refactor codemod into core codemod * semgrep subprocess shell false codemod
1 parent 897a0d6 commit c647ec2

File tree

6 files changed

+115
-19
lines changed

6 files changed

+115
-19
lines changed

integration_tests/test_subprocess_shell_false.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from codemodder.codemods.test import BaseIntegrationTest
2-
from core_codemods.subprocess_shell_false import SubprocessShellFalse
2+
from core_codemods.subprocess_shell_false import (
3+
SubprocessShellFalse,
4+
SubprocessShellFalseTransformer,
5+
)
36

47

58
class TestSubprocessShellFalse(BaseIntegrationTest):
@@ -12,6 +15,6 @@ class TestSubprocessShellFalse(BaseIntegrationTest):
1215

1316
expected_diff = "--- \n+++ \n@@ -1,2 +1,2 @@\n import subprocess\n-subprocess.run(['ls', '-l'], shell=True)\n+subprocess.run(['ls', '-l'], shell=False)\n"
1417
expected_line_change = "2"
15-
change_description = SubprocessShellFalse.change_description
18+
change_description = SubprocessShellFalseTransformer.change_description
1619
# expected because output code points to fake file
1720
allowed_exceptions = (FileNotFoundError,)

src/codemodder/scripts/generate_docs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ class DocMetadata:
324324
"enable-jinja2-autoescape",
325325
"jwt-decode-verify",
326326
"use-defusedxml",
327+
"subprocess-shell-false",
327328
]
328329
SEMGREP_CODEMODS = {
329330
name: DocMetadata(

src/core_codemods/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from .secure_random import SecureRandom
5757
from .semgrep.semgrep_enable_jinja2_autoescape import SemgrepEnableJinja2Autoescape
5858
from .semgrep.semgrep_jwt_decode_verify import SemgrepJwtDecodeVerify
59+
from .semgrep.semgrep_subprocess_shell_false import SemgrepSubprocessShellFalse
5960
from .semgrep.semgrep_use_defused_xml import SemgrepUseDefusedXml
6061
from .sonar.sonar_break_or_continue_out_of_loop import SonarBreakOrContinueOutOfLoop
6162
from .sonar.sonar_disable_graphql_introspection import SonarDisableGraphQLIntrospection
@@ -202,5 +203,6 @@
202203
SemgrepEnableJinja2Autoescape,
203204
SemgrepJwtDecodeVerify,
204205
SemgrepUseDefusedXml,
206+
SemgrepSubprocessShellFalse,
205207
],
206208
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from core_codemods.semgrep.api import SemgrepCodemod
2+
from core_codemods.subprocess_shell_false import SubprocessShellFalse
3+
4+
SemgrepSubprocessShellFalse = SemgrepCodemod.from_core_codemod(
5+
name="subprocess-shell-false",
6+
other=SubprocessShellFalse,
7+
rule_id="python.lang.security.audit.subprocess-shell-true.subprocess-shell-true",
8+
rule_name="subprocess-shell-true",
9+
)

src/core_codemods/subprocess_shell_false.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,22 @@
33
from libcst.metadata import ParentNodeProvider
44

55
from codemodder.codemods.check_annotations import is_disabled_by_annotations
6-
from codemodder.codemods.libcst_transformer import NewArg
6+
from codemodder.codemods.libcst_transformer import (
7+
LibcstResultTransformer,
8+
LibcstTransformerPipeline,
9+
NewArg,
10+
)
711
from codemodder.codemods.utils_mixin import NameResolutionMixin
8-
from core_codemods.api import Metadata, Reference, ReviewGuidance, SimpleCodemod
12+
from core_codemods.api import (
13+
CoreCodemod,
14+
Metadata,
15+
Reference,
16+
ReviewGuidance,
17+
SimpleCodemod,
18+
)
919

1020

11-
class SubprocessShellFalse(SimpleCodemod, NameResolutionMixin):
12-
metadata = Metadata(
13-
name="subprocess-shell-false",
14-
summary="Use `shell=False` in `subprocess` Function Calls",
15-
review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
16-
references=[
17-
Reference(
18-
url="https://docs.python.org/3/library/subprocess.html#security-considerations"
19-
),
20-
Reference(
21-
url="https://en.wikipedia.org/wiki/Code_injection#Shell_injection"
22-
),
23-
Reference(url="https://stackoverflow.com/a/3172488"),
24-
],
25-
)
21+
class SubprocessShellFalseTransformer(LibcstResultTransformer, NameResolutionMixin):
2622
change_description = "Set `shell` keyword argument to `False`"
2723
SUBPROCESS_FUNCS = [
2824
f"subprocess.{func}"
@@ -68,3 +64,22 @@ def first_arg_is_not_string(self, original_node: cst.Call) -> bool:
6864
value=m.SimpleString() | m.ConcatenatedString() | m.FormattedString()
6965
),
7066
)
67+
68+
69+
SubprocessShellFalse = CoreCodemod(
70+
metadata=Metadata(
71+
name="subprocess-shell-false",
72+
summary="Use `shell=False` in `subprocess` Function Calls",
73+
review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
74+
references=[
75+
Reference(
76+
url="https://docs.python.org/3/library/subprocess.html#security-considerations"
77+
),
78+
Reference(
79+
url="https://en.wikipedia.org/wiki/Code_injection#Shell_injection"
80+
),
81+
Reference(url="https://stackoverflow.com/a/3172488"),
82+
],
83+
),
84+
transformer=LibcstTransformerPipeline(SubprocessShellFalseTransformer),
85+
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import json
2+
3+
from codemodder.codemods.test import BaseSASTCodemodTest
4+
from core_codemods.semgrep.semgrep_subprocess_shell_false import (
5+
SemgrepSubprocessShellFalse,
6+
)
7+
8+
9+
class TestSemgrepSubprocessShellFalse(BaseSASTCodemodTest):
10+
codemod = SemgrepSubprocessShellFalse
11+
tool = "semgrep"
12+
13+
def test_name(self):
14+
assert self.codemod.name == "subprocess-shell-false"
15+
16+
def test_import(self, tmpdir):
17+
input_code = """\
18+
from subprocess import run
19+
run(args, shell=True)
20+
"""
21+
expexted_output = """\
22+
from subprocess import run
23+
run(args, shell=False)
24+
"""
25+
26+
results = {
27+
"runs": [
28+
{
29+
"results": [
30+
{
31+
"fingerprints": {"matchBasedId/v1": "123"},
32+
"locations": [
33+
{
34+
"physicalLocation": {
35+
"artifactLocation": {
36+
"uri": "code.py",
37+
"uriBaseId": "%SRCROOT%",
38+
},
39+
"region": {
40+
"endColumn": 22,
41+
"endLine": 2,
42+
"snippet": {
43+
"text": "run(args, shell=True)"
44+
},
45+
"startColumn": 1,
46+
"startLine": 2,
47+
},
48+
}
49+
}
50+
],
51+
"message": {
52+
"text": "Found 'subprocess' function 'run' with 'shell=True'. This is dangerous because this call will spawn the command using a shell process. Doing so propagates current shell settings and variables, which makes it much easier for a malicious actor to execute commands. Use 'shell=False' instead."
53+
},
54+
"properties": {},
55+
"ruleId": "python.lang.security.audit.subprocess-shell-true.subprocess-shell-true",
56+
}
57+
]
58+
}
59+
]
60+
}
61+
self.run_and_assert(
62+
tmpdir,
63+
input_code,
64+
expexted_output,
65+
results=json.dumps(results),
66+
)

0 commit comments

Comments
 (0)