Skip to content

Commit 1460393

Browse files
authored
sonar url-sandbox (#445)
add sonar url sandbox codemod
1 parent f632411 commit 1460393

File tree

9 files changed

+222
-44
lines changed

9 files changed

+222
-44
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from codemodder.codemods.test import SonarIntegrationTest
2+
from core_codemods.sonar.sonar_url_sandbox import SonarUrlSandbox
3+
from core_codemods.url_sandbox import UrlSandboxTransformer
4+
5+
6+
class TestSonarUrlSandbox(SonarIntegrationTest):
7+
codemod = SonarUrlSandbox
8+
code_path = "tests/samples/flask_request.py"
9+
replacement_lines = [
10+
(1, "from flask import Flask, request\n"),
11+
(2, "from security import safe_requests\n"),
12+
(10, " safe_requests.get(url)\n"),
13+
]
14+
expected_diff = '--- \n+++ \n@@ -1,5 +1,5 @@\n-import requests\n from flask import Flask, request\n+from security import safe_requests\n \n app = Flask(__name__)\n \n@@ -7,4 +7,4 @@\n @app.route("/example")\n def example():\n url = request.args["url"]\n- requests.get(url)\n+ safe_requests.get(url)\n'
15+
expected_line_change = "10"
16+
change_description = UrlSandboxTransformer.change_description

integration_tests/test_url_sandbox.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from codemodder.codemods.test import BaseIntegrationTest
22
from codemodder.dependency import Security
3-
from core_codemods.url_sandbox import UrlSandbox
3+
from core_codemods.url_sandbox import UrlSandbox, UrlSandboxTransformer
44

55

66
class TestUrlSandbox(BaseIntegrationTest):
@@ -32,7 +32,7 @@ class TestUrlSandbox(BaseIntegrationTest):
3232
"""
3333

3434
expected_line_change = "5"
35-
change_description = UrlSandbox.change_description
35+
change_description = UrlSandboxTransformer.change_description
3636
num_changed_files = 2
3737

3838
requirements_file_name = "requirements.txt"

src/codemodder/scripts/generate_docs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,11 @@ class DocMetadata:
339339
guidance_explained="This change is generally safe and will prevent deserialization vulnerabilities.",
340340
need_sarif="Yes (DefectDojo)",
341341
),
342+
"url-sandbox-S5144": DocMetadata(
343+
importance=CORE_METADATA["url-sandbox"].importance,
344+
guidance_explained=CORE_METADATA["url-sandbox"].guidance_explained,
345+
need_sarif="Yes (Sonar)",
346+
),
342347
}
343348

344349

src/core_codemods/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
)
6565
from .sonar.sonar_secure_random import SonarSecureRandom
6666
from .sonar.sonar_tempfile_mktemp import SonarTempfileMktemp
67+
from .sonar.sonar_url_sandbox import SonarUrlSandbox
6768
from .sql_parameterization import SQLQueryParameterization
6869
from .str_concat_in_seq_literal import StrConcatInSeqLiteral
6970
from .subprocess_shell_false import SubprocessShellFalse
@@ -156,6 +157,7 @@
156157
SonarTempfileMktemp,
157158
SonarSecureRandom,
158159
SonarEnableJinja2Autoescape,
160+
SonarUrlSandbox,
159161
],
160162
)
161163

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from codemodder.codemods.sonar import SonarCodemod
2+
from core_codemods.url_sandbox import UrlSandbox
3+
4+
SonarUrlSandbox = SonarCodemod.from_core_codemod(
5+
name="url-sandbox-S5144",
6+
other=UrlSandbox,
7+
rule_id="pythonsecurity:S5144",
8+
rule_name="Server-side requests should not be vulnerable to forging attacks",
9+
rule_url="https://rules.sonarsource.com/python/type/Vulnerability/RSPEC-5144/",
10+
)

src/core_codemods/url_sandbox.py

Lines changed: 50 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,26 @@
77
from libcst.metadata import PositionProvider, ScopeProvider
88

99
from codemodder.codemods.base_visitor import UtilsMixin
10+
from codemodder.codemods.libcst_transformer import (
11+
LibcstResultTransformer,
12+
LibcstTransformerPipeline,
13+
)
14+
from codemodder.codemods.semgrep import SemgrepRuleDetector
1015
from codemodder.codemods.transformations.remove_unused_imports import (
1116
RemoveUnusedImportsCodemod,
1217
)
1318
from codemodder.codemods.utils import ReplaceNodes
1419
from codemodder.codetf import Change
1520
from codemodder.dependency import Security
1621
from codemodder.file_context import FileContext
17-
from core_codemods.api import Metadata, Reference, ReviewGuidance, SimpleCodemod
22+
from core_codemods.api import CoreCodemod, Metadata, Reference, ReviewGuidance
1823

1924
replacement_import = "safe_requests"
2025

2126

22-
class UrlSandbox(SimpleCodemod):
23-
metadata = Metadata(
24-
name="url-sandbox",
25-
summary="Sandbox URL Creation",
26-
review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
27-
references=[
28-
Reference(
29-
url="https://github.com/pixee/python-security/blob/main/src/security/safe_requests/api.py"
30-
),
31-
Reference(url="https://portswigger.net/web-security/ssrf"),
32-
Reference(
33-
url="https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"
34-
),
35-
Reference(
36-
url="https://www.rapid7.com/blog/post/2021/11/23/owasp-top-10-deep-dive-defending-against-server-side-request-forgery/"
37-
),
38-
Reference(url="https://blog.assetnote.io/2021/01/13/blind-ssrf-chains/"),
39-
],
40-
)
27+
class UrlSandboxTransformer(LibcstResultTransformer):
4128
change_description = "Switch use of requests for security.safe_requests"
42-
43-
detector_pattern = """
44-
rules:
45-
- id: url-sandbox
46-
message: Unbounded URL creation
47-
severity: WARNING
48-
languages:
49-
- python
50-
pattern-either:
51-
- patterns:
52-
- pattern: requests.get(...)
53-
- pattern-not: requests.get("...")
54-
- pattern-inside: |
55-
import requests
56-
...
57-
"""
58-
5929
METADATA_DEPENDENCIES = (PositionProvider, ScopeProvider)
60-
6130
adds_dependency = True
6231

6332
def transform_module_impl(self, tree: cst.Module) -> cst.Module:
@@ -111,7 +80,7 @@ def __init__(
11180
)
11281

11382
def leave_Call(self, original_node: cst.Call):
114-
if not (self.node_is_selected(original_node)):
83+
if not self.node_is_selected(original_node):
11584
return
11685

11786
line_number = self.node_position(original_node).start.line
@@ -139,7 +108,7 @@ def leave_Call(self, original_node: cst.Call):
139108
self.changes_in_file.append(
140109
Change(
141110
lineNumber=line_number,
142-
description=UrlSandbox.change_description,
111+
description=UrlSandboxTransformer.change_description,
143112
)
144113
)
145114

@@ -156,7 +125,7 @@ def leave_Call(self, original_node: cst.Call):
156125
self.changes_in_file.append(
157126
Change(
158127
lineNumber=line_number,
159-
description=UrlSandbox.change_description,
128+
description=UrlSandboxTransformer.change_description,
160129
)
161130
)
162131

@@ -175,3 +144,43 @@ def find_single_assignment(self, node: CSTNode) -> Optional[CSTNode]:
175144
if len(assignments) == 1:
176145
return next(iter(assignments)).node
177146
return None
147+
148+
149+
UrlSandbox = CoreCodemod(
150+
metadata=Metadata(
151+
name="url-sandbox",
152+
summary="Sandbox URL Creation",
153+
review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
154+
references=[
155+
Reference(
156+
url="https://github.com/pixee/python-security/blob/main/src/security/safe_requests/api.py"
157+
),
158+
Reference(url="https://portswigger.net/web-security/ssrf"),
159+
Reference(
160+
url="https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"
161+
),
162+
Reference(
163+
url="https://www.rapid7.com/blog/post/2021/11/23/owasp-top-10-deep-dive-defending-against-server-side-request-forgery/"
164+
),
165+
Reference(url="https://blog.assetnote.io/2021/01/13/blind-ssrf-chains/"),
166+
],
167+
),
168+
detector=SemgrepRuleDetector(
169+
"""
170+
rules:
171+
- id: url-sandbox
172+
message: Unbounded URL creation
173+
severity: WARNING
174+
languages:
175+
- python
176+
pattern-either:
177+
- patterns:
178+
- pattern: requests.get(...)
179+
- pattern-not: requests.get("...")
180+
- pattern-inside: |
181+
import requests
182+
...
183+
"""
184+
),
185+
transformer=LibcstTransformerPipeline(UrlSandboxTransformer),
186+
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import json
2+
3+
from codemodder.codemods.test import BaseSASTCodemodTest
4+
from core_codemods.sonar.sonar_url_sandbox import SonarUrlSandbox
5+
6+
7+
class TestSonarUrlSandbox(BaseSASTCodemodTest):
8+
codemod = SonarUrlSandbox
9+
tool = "sonar"
10+
11+
def test_name(self):
12+
assert self.codemod.name == "url-sandbox-S5144"
13+
14+
def test_simple(self, tmpdir):
15+
input_code = """
16+
import requests
17+
from flask import Flask, request
18+
19+
app = Flask(__name__)
20+
21+
22+
@app.route("/example")
23+
def example():
24+
url = request.args["url"]
25+
requests.get(url)
26+
"""
27+
expected = """
28+
from flask import Flask, request
29+
from security import safe_requests
30+
31+
app = Flask(__name__)
32+
33+
34+
@app.route("/example")
35+
def example():
36+
url = request.args["url"]
37+
safe_requests.get(url)
38+
"""
39+
issues = {
40+
"issues": [
41+
{
42+
"rule": "pythonsecurity:S5144",
43+
"status": "OPEN",
44+
"component": "code.py",
45+
"textRange": {
46+
"startLine": 11,
47+
"endLine": 11,
48+
"startOffset": 4,
49+
"endOffset": 21,
50+
},
51+
}
52+
]
53+
}
54+
self.run_and_assert(tmpdir, input_code, expected, results=json.dumps(issues))

tests/samples/flask_request.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import requests
2+
from flask import Flask, request
3+
4+
app = Flask(__name__)
5+
6+
7+
@app.route("/example")
8+
def example():
9+
url = request.args["url"]
10+
requests.get(url)

tests/samples/sonar_issues.json

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"total":37,
2+
"total":38,
33
"p":1,
44
"ps":500,
55
"paging":{
@@ -1608,6 +1608,78 @@
16081608
"severity":"HIGH"
16091609
}
16101610
]
1611+
},
1612+
{
1613+
"key": "AY6o8XTbdw-0_54hqn7F",
1614+
"rule": "pythonsecurity:S5144",
1615+
"severity": "MAJOR",
1616+
"component": "pixee_codemodder-python:flask_request.py",
1617+
"project": "pixee_codemodder-python",
1618+
"line": 10,
1619+
"hash": "c864c19608fe39f9e580a5deefb67381",
1620+
"textRange": {
1621+
"startLine": 10,
1622+
"endLine": 10,
1623+
"startOffset": 4,
1624+
"endOffset": 21
1625+
},
1626+
"flows": [
1627+
{
1628+
"locations": [
1629+
{
1630+
"component": "pixee_codemodder-python:flask_request.py",
1631+
"textRange": {
1632+
"startLine": 10,
1633+
"endLine": 10,
1634+
"startOffset": 4,
1635+
"endOffset": 21
1636+
},
1637+
"msg": "Sink: this invocation is not safe; a malicious value can be used as argument"
1638+
},
1639+
{
1640+
"component": "pixee_codemodder-python:flask_request.py",
1641+
"textRange": {
1642+
"startLine": 9,
1643+
"endLine": 9,
1644+
"startOffset": 4,
1645+
"endOffset": 29
1646+
},
1647+
"msg": "A malicious value can be assigned to variable ‘url’"
1648+
},
1649+
{
1650+
"component": "pixee_codemodder-python:flask_request.py",
1651+
"textRange": {
1652+
"startLine": 9,
1653+
"endLine": 9,
1654+
"startOffset": 10,
1655+
"endOffset": 29
1656+
},
1657+
"msg": "Source: a user can craft an HTTP request with malicious content"
1658+
}
1659+
]
1660+
}
1661+
],
1662+
"status": "OPEN",
1663+
"message": "Change this code to not construct the URL from user-controlled data.",
1664+
"effort": "30min",
1665+
"debt": "30min",
1666+
"assignee": "clavedeluna@github",
1667+
"tags": [
1668+
"cwe"
1669+
],
1670+
"creationDate": "2024-04-04T13:49:07+0200",
1671+
"updateDate": "2024-04-04T13:49:25+0200",
1672+
"type": "VULNERABILITY",
1673+
"organization": "pixee",
1674+
"pullRequest": "445",
1675+
"cleanCodeAttribute": "COMPLETE",
1676+
"cleanCodeAttributeCategory": "INTENTIONAL",
1677+
"impacts": [
1678+
{
1679+
"softwareQuality": "SECURITY",
1680+
"severity": "MEDIUM"
1681+
}
1682+
]
16111683
}
16121684
],
16131685
"components":[

0 commit comments

Comments
 (0)