diff --git a/src/codemodder/scripts/generate_docs.py b/src/codemodder/scripts/generate_docs.py index d66743a2..6d7aa606 100644 --- a/src/codemodder/scripts/generate_docs.py +++ b/src/codemodder/scripts/generate_docs.py @@ -321,6 +321,7 @@ class DocMetadata: } SEMGREP_CODEMOD_NAMES = [ + "url-sandbox", "enable-jinja2-autoescape", "jwt-decode-verify", "use-defusedxml", diff --git a/src/core_codemods/__init__.py b/src/core_codemods/__init__.py index 8d3e7b77..7b07b1e4 100644 --- a/src/core_codemods/__init__.py +++ b/src/core_codemods/__init__.py @@ -61,6 +61,7 @@ from .semgrep.semgrep_rsa_key_size import SemgrepRsaKeySize from .semgrep.semgrep_sql_parametrization import SemgrepSQLParameterization from .semgrep.semgrep_subprocess_shell_false import SemgrepSubprocessShellFalse +from .semgrep.semgrep_url_sandbox import SemgrepUrlSandbox 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 @@ -204,6 +205,7 @@ semgrep_registry = CodemodCollection( origin="semgrep", codemods=[ + SemgrepUrlSandbox, SemgrepEnableJinja2Autoescape, SemgrepJwtDecodeVerify, SemgrepUseDefusedXml, diff --git a/src/core_codemods/semgrep/semgrep_url_sandbox.py b/src/core_codemods/semgrep/semgrep_url_sandbox.py new file mode 100644 index 00000000..90e5450c --- /dev/null +++ b/src/core_codemods/semgrep/semgrep_url_sandbox.py @@ -0,0 +1,23 @@ +from core_codemods.semgrep.api import SemgrepCodemod, ToolRule, semgrep_url_from_id +from core_codemods.url_sandbox import UrlSandbox + +SemgrepUrlSandbox = SemgrepCodemod.from_core_codemod( + name="url-sandbox", + other=UrlSandbox, + rules=[ + ToolRule( + id=( + rule_id := "python.django.security.injection.ssrf.ssrf-injection-requests.ssrf-injection-requests" + ), + name="ssrf-injection-requests", + url=semgrep_url_from_id(rule_id), + ), + ToolRule( + id=( + rule_id := "python.flask.security.injection.ssrf-requests.ssrf-requests" + ), + name="ssrf-requests", + url=semgrep_url_from_id(rule_id), + ), + ], +) diff --git a/tests/codemods/semgrep/test_semgrep_url_sandbox.py b/tests/codemods/semgrep/test_semgrep_url_sandbox.py new file mode 100644 index 00000000..82a619a4 --- /dev/null +++ b/tests/codemods/semgrep/test_semgrep_url_sandbox.py @@ -0,0 +1,109 @@ +import json + +from codemodder.codemods.test import BaseSASTCodemodTest +from core_codemods.semgrep.semgrep_url_sandbox import SemgrepUrlSandbox + + +class TestSemgrepUrlSandbox(BaseSASTCodemodTest): + codemod = SemgrepUrlSandbox + tool = "semgrep" + + def test_name(self): + assert self.codemod.name == "url-sandbox" + + def test_url_sandbox(self, tmpdir): + original_code = """\ + import requests + from flask import Flask, request + + app = Flask(__name__) + + + @app.route("/example") + def example(): + url = request.args["url"] + requests.get(url) + """ + + new_code = """\ + from flask import Flask, request + from security import safe_requests + + app = Flask(__name__) + + + @app.route("/example") + def example(): + url = request.args["url"] + safe_requests.get(url) + """ + + results = { + "runs": [ + { + "results": [ + { + "fingerprints": {"matchBasedId/v1": "370059975f"}, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "code.py", + "uriBaseId": "%SRCROOT%", + }, + "region": { + "endColumn": 22, + "endLine": 10, + "snippet": { + "text": ' url = request.args["url"]\n requests.get(url)' + }, + "startColumn": 5, + "startLine": 9, + }, + } + } + ], + "message": { + "text": "Data from request object is passed to a new server-side request. This could lead to a server-side request forgery (SSRF). To mitigate, ensure that schemes and hosts are validated against an allowlist, do not forward the response to the user, and ensure proper authentication and transport-layer security in the proxied request. See https://owasp.org/www-community/attacks/Server_Side_Request_Forgery to learn more about SSRF vulnerabilities." + }, + "properties": {}, + "ruleId": "python.django.security.injection.ssrf.ssrf-injection-requests.ssrf-injection-requests", + }, + { + "fingerprints": {"matchBasedId/v1": "cd899"}, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "code.py", + "uriBaseId": "%SRCROOT%", + }, + "region": { + "endColumn": 22, + "endLine": 10, + "snippet": { + "text": " requests.get(url)" + }, + "startColumn": 5, + "startLine": 10, + }, + } + } + ], + "message": { + "text": "Data from request object is passed to a new server-side request. This could lead to a server-side request forgery (SSRF). To mitigate, ensure that schemes and hosts are validated against an allowlist, do not forward the response to the user, and ensure proper authentication and transport-layer security in the proxied request." + }, + "properties": {}, + "ruleId": "python.flask.security.injection.ssrf-requests.ssrf-requests", + }, + ], + } + ] + } + + self.run_and_assert( + tmpdir, + original_code, + new_code, + results=json.dumps(results), + )