Skip to content

Commit a95f3e7

Browse files
authored
New Semgrep codemod: sandbox process creation (#831)
1 parent f394e5a commit a95f3e7

File tree

4 files changed

+129
-2
lines changed

4 files changed

+129
-2
lines changed

src/codemodder/semgrep.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,18 @@ class SemgrepLocation(SarifLocation):
2828
def from_sarif(cls, sarif_location) -> Self:
2929
artifact_location = sarif_location["physicalLocation"]["artifactLocation"]
3030
file = Path(artifact_location["uri"])
31+
snippet = (
32+
sarif_location["physicalLocation"]["region"].get("snippet", {}).get("text")
33+
)
3134
start = LineInfo(
3235
line=sarif_location["physicalLocation"]["region"]["startLine"],
3336
column=sarif_location["physicalLocation"]["region"]["startColumn"],
34-
snippet=sarif_location["physicalLocation"]["region"]["snippet"]["text"],
37+
snippet=snippet,
3538
)
3639
end = LineInfo(
3740
line=sarif_location["physicalLocation"]["region"]["endLine"],
3841
column=sarif_location["physicalLocation"]["region"]["endColumn"],
39-
snippet=sarif_location["physicalLocation"]["region"]["snippet"]["text"],
42+
snippet=snippet,
4043
)
4144
return cls(file=file, start=start, end=end)
4245

src/core_codemods/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
from .semgrep.semgrep_nan_injection import SemgrepNanInjection
6363
from .semgrep.semgrep_no_csrf_exempt import SemgrepNoCsrfExempt
6464
from .semgrep.semgrep_rsa_key_size import SemgrepRsaKeySize
65+
from .semgrep.semgrep_sandbox_process_creation import SemgrepSandboxProcessCreation
6566
from .semgrep.semgrep_sql_parameterization import SemgrepSQLParameterization
6667
from .semgrep.semgrep_subprocess_shell_false import SemgrepSubprocessShellFalse
6768
from .semgrep.semgrep_url_sandbox import SemgrepUrlSandbox
@@ -222,6 +223,7 @@
222223
SemgrepNoCsrfExempt,
223224
SemgrepJwtDecodeVerify,
224225
SemgrepUseDefusedXml,
226+
SemgrepSandboxProcessCreation,
225227
SemgrepSubprocessShellFalse,
226228
SemgrepDjangoSecureSetCookie,
227229
SemgrepHardenPyyaml,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from core_codemods.process_creation_sandbox import ProcessSandbox
2+
from core_codemods.semgrep.api import SemgrepCodemod, ToolRule, semgrep_url_from_id
3+
4+
SemgrepSandboxProcessCreation = SemgrepCodemod.from_core_codemod(
5+
name="sandbox-process-creation",
6+
other=ProcessSandbox(),
7+
rules=[
8+
ToolRule(
9+
id=(
10+
rule_id := "python.lang.security.dangerous-system-call.dangerous-system-call"
11+
),
12+
name="dangerous-system-call",
13+
url=semgrep_url_from_id(rule_id),
14+
),
15+
],
16+
)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import mock
2+
3+
from codemodder.codemods.test import BaseSASTCodemodTest
4+
from codemodder.dependency import Security
5+
from core_codemods.semgrep.semgrep_sandbox_process_creation import (
6+
SemgrepSandboxProcessCreation,
7+
)
8+
9+
10+
class TestSonarSandboxProcessCreation(BaseSASTCodemodTest):
11+
codemod = SemgrepSandboxProcessCreation
12+
tool = "semgrep"
13+
14+
def test_name(self):
15+
assert self.codemod.name == "sandbox-process-creation"
16+
17+
@mock.patch("codemodder.codemods.api.FileContext.add_dependency")
18+
def test_simple(self, adds_dependency, tmpdir):
19+
input_code = """
20+
import os
21+
from flask import render_template, request
22+
23+
@app.route('/vuln', methods=['GET', 'POST'])
24+
def vuln():
25+
output = ""
26+
if request.method == 'POST':
27+
command = request.form.get('command')
28+
output = os.popen(command).read()
29+
return render_template('vuln.html', output=output)
30+
""".lstrip(
31+
"\n"
32+
)
33+
expected = """
34+
import os
35+
from flask import render_template, request
36+
from security import safe_command
37+
38+
@app.route('/vuln', methods=['GET', 'POST'])
39+
def vuln():
40+
output = ""
41+
if request.method == 'POST':
42+
command = request.form.get('command')
43+
output = safe_command.run(os.popen, command).read()
44+
return render_template('vuln.html', output=output)
45+
""".lstrip(
46+
"\n"
47+
)
48+
self.run_and_assert(tmpdir, input_code, expected, results=SARIF)
49+
adds_dependency.assert_called_once_with(Security)
50+
51+
52+
SARIF = """
53+
{
54+
"runs": [
55+
{
56+
"automationDetails": {
57+
"id": ".github/workflows/semgrep.yml:semgrep_scan/"
58+
},
59+
"conversion": {
60+
"tool": {
61+
"driver": {
62+
"name": "GitHub Code Scanning"
63+
}
64+
}
65+
},
66+
"results": [
67+
{
68+
"correlationGuid": "a90240a2-8d09-47eb-a1c5-0af9d5b225c9",
69+
"level": "error",
70+
"locations": [
71+
{
72+
"physicalLocation": {
73+
"artifactLocation": {
74+
"index": 1,
75+
"uri": "code.py"
76+
},
77+
"region": {
78+
"endColumn": 35,
79+
"endLine": 9,
80+
"startColumn": 18,
81+
"startLine": 9
82+
}
83+
}
84+
}
85+
],
86+
"message": {
87+
"text": "Found user-controlled data used in a system call. This could allow a malicious actor to execute commands. Use the 'subprocess' module instead, which is easier to use without accidentally exposing a command injection vulnerability."
88+
},
89+
"partialFingerprints": {
90+
"primaryLocationLineHash": "b897622e8906ac69:1"
91+
},
92+
"properties": {
93+
"github/alertNumber": 2,
94+
"github/alertUrl": "https://api.github.com/repos/nahsra/vulnerable-app-sample/code-scanning/alerts/2"
95+
},
96+
"rule": {
97+
"id": "python.lang.security.dangerous-system-call.dangerous-system-call",
98+
"index": 723
99+
},
100+
"ruleId": "python.lang.security.dangerous-system-call.dangerous-system-call"
101+
}
102+
]
103+
}
104+
]
105+
}
106+
"""

0 commit comments

Comments
 (0)