Skip to content

Commit

Permalink
Semgrep subprocess shell False codemod (#706)
Browse files Browse the repository at this point in the history
* refactor codemod into core codemod

* semgrep subprocess shell false codemod
  • Loading branch information
clavedeluna authored Jul 10, 2024
1 parent 897a0d6 commit c647ec2
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 19 deletions.
7 changes: 5 additions & 2 deletions integration_tests/test_subprocess_shell_false.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from codemodder.codemods.test import BaseIntegrationTest
from core_codemods.subprocess_shell_false import SubprocessShellFalse
from core_codemods.subprocess_shell_false import (
SubprocessShellFalse,
SubprocessShellFalseTransformer,
)


class TestSubprocessShellFalse(BaseIntegrationTest):
Expand All @@ -12,6 +15,6 @@ class TestSubprocessShellFalse(BaseIntegrationTest):

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"
expected_line_change = "2"
change_description = SubprocessShellFalse.change_description
change_description = SubprocessShellFalseTransformer.change_description
# expected because output code points to fake file
allowed_exceptions = (FileNotFoundError,)
1 change: 1 addition & 0 deletions src/codemodder/scripts/generate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ class DocMetadata:
"enable-jinja2-autoescape",
"jwt-decode-verify",
"use-defusedxml",
"subprocess-shell-false",
]
SEMGREP_CODEMODS = {
name: DocMetadata(
Expand Down
2 changes: 2 additions & 0 deletions src/core_codemods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
from .secure_random import SecureRandom
from .semgrep.semgrep_enable_jinja2_autoescape import SemgrepEnableJinja2Autoescape
from .semgrep.semgrep_jwt_decode_verify import SemgrepJwtDecodeVerify
from .semgrep.semgrep_subprocess_shell_false import SemgrepSubprocessShellFalse
from .semgrep.semgrep_use_defused_xml import SemgrepUseDefusedXml
from .sonar.sonar_break_or_continue_out_of_loop import SonarBreakOrContinueOutOfLoop
from .sonar.sonar_disable_graphql_introspection import SonarDisableGraphQLIntrospection
Expand Down Expand Up @@ -202,5 +203,6 @@
SemgrepEnableJinja2Autoescape,
SemgrepJwtDecodeVerify,
SemgrepUseDefusedXml,
SemgrepSubprocessShellFalse,
],
)
9 changes: 9 additions & 0 deletions src/core_codemods/semgrep/semgrep_subprocess_shell_false.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from core_codemods.semgrep.api import SemgrepCodemod
from core_codemods.subprocess_shell_false import SubprocessShellFalse

SemgrepSubprocessShellFalse = SemgrepCodemod.from_core_codemod(
name="subprocess-shell-false",
other=SubprocessShellFalse,
rule_id="python.lang.security.audit.subprocess-shell-true.subprocess-shell-true",
rule_name="subprocess-shell-true",
)
49 changes: 32 additions & 17 deletions src/core_codemods/subprocess_shell_false.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,22 @@
from libcst.metadata import ParentNodeProvider

from codemodder.codemods.check_annotations import is_disabled_by_annotations
from codemodder.codemods.libcst_transformer import NewArg
from codemodder.codemods.libcst_transformer import (
LibcstResultTransformer,
LibcstTransformerPipeline,
NewArg,
)
from codemodder.codemods.utils_mixin import NameResolutionMixin
from core_codemods.api import Metadata, Reference, ReviewGuidance, SimpleCodemod
from core_codemods.api import (
CoreCodemod,
Metadata,
Reference,
ReviewGuidance,
SimpleCodemod,
)


class SubprocessShellFalse(SimpleCodemod, NameResolutionMixin):
metadata = Metadata(
name="subprocess-shell-false",
summary="Use `shell=False` in `subprocess` Function Calls",
review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
references=[
Reference(
url="https://docs.python.org/3/library/subprocess.html#security-considerations"
),
Reference(
url="https://en.wikipedia.org/wiki/Code_injection#Shell_injection"
),
Reference(url="https://stackoverflow.com/a/3172488"),
],
)
class SubprocessShellFalseTransformer(LibcstResultTransformer, NameResolutionMixin):
change_description = "Set `shell` keyword argument to `False`"
SUBPROCESS_FUNCS = [
f"subprocess.{func}"
Expand Down Expand Up @@ -68,3 +64,22 @@ def first_arg_is_not_string(self, original_node: cst.Call) -> bool:
value=m.SimpleString() | m.ConcatenatedString() | m.FormattedString()
),
)


SubprocessShellFalse = CoreCodemod(
metadata=Metadata(
name="subprocess-shell-false",
summary="Use `shell=False` in `subprocess` Function Calls",
review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
references=[
Reference(
url="https://docs.python.org/3/library/subprocess.html#security-considerations"
),
Reference(
url="https://en.wikipedia.org/wiki/Code_injection#Shell_injection"
),
Reference(url="https://stackoverflow.com/a/3172488"),
],
),
transformer=LibcstTransformerPipeline(SubprocessShellFalseTransformer),
)
66 changes: 66 additions & 0 deletions tests/codemods/semgrep/test_semgrep_subprocess_shell_false.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import json

from codemodder.codemods.test import BaseSASTCodemodTest
from core_codemods.semgrep.semgrep_subprocess_shell_false import (
SemgrepSubprocessShellFalse,
)


class TestSemgrepSubprocessShellFalse(BaseSASTCodemodTest):
codemod = SemgrepSubprocessShellFalse
tool = "semgrep"

def test_name(self):
assert self.codemod.name == "subprocess-shell-false"

def test_import(self, tmpdir):
input_code = """\
from subprocess import run
run(args, shell=True)
"""
expexted_output = """\
from subprocess import run
run(args, shell=False)
"""

results = {
"runs": [
{
"results": [
{
"fingerprints": {"matchBasedId/v1": "123"},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "code.py",
"uriBaseId": "%SRCROOT%",
},
"region": {
"endColumn": 22,
"endLine": 2,
"snippet": {
"text": "run(args, shell=True)"
},
"startColumn": 1,
"startLine": 2,
},
}
}
],
"message": {
"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."
},
"properties": {},
"ruleId": "python.lang.security.audit.subprocess-shell-true.subprocess-shell-true",
}
]
}
]
}
self.run_and_assert(
tmpdir,
input_code,
expexted_output,
results=json.dumps(results),
)

0 comments on commit c647ec2

Please sign in to comment.