Skip to content

Commit adeef45

Browse files
authored
semgrep remove scrf exempt decorator (#755)
1 parent d6c64ed commit adeef45

File tree

4 files changed

+186
-0
lines changed

4 files changed

+186
-0
lines changed

src/codemodder/scripts/generate_docs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ class DocMetadata:
347347
guidance_explained="This codemod updates the key size for RSA to the `2048` recommended minimum. Since this is an important security fix that follows modern cryptographic standards, we believe this change can be safely merged without review.",
348348
need_sarif="Yes (Semgrep)",
349349
),
350+
"no-csrf-exempt": DocMetadata(
351+
importance="Medium",
352+
guidance_explained="This codemod removes the `@csrf_exempt` decorator from a Django view to ensure it's protected against CSRF attacks. However, there are valid cases for using this decorator so make sure to review your application to determine if this is the case.",
353+
need_sarif="Yes (Semgrep)",
354+
),
350355
}
351356
ALL_CODEMODS_METADATA = (
352357
CORE_CODEMODS | DEFECTDOJO_CODEMODS | SONAR_CODEMODS | SEMGREP_CODEMODS

src/core_codemods/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from .semgrep.semgrep_enable_jinja2_autoescape import SemgrepEnableJinja2Autoescape
5959
from .semgrep.semgrep_harden_pyyaml import SemgrepHardenPyyaml
6060
from .semgrep.semgrep_jwt_decode_verify import SemgrepJwtDecodeVerify
61+
from .semgrep.semgrep_no_csrf_exempt import SemgrepNoCsrfExempt
6162
from .semgrep.semgrep_rsa_key_size import SemgrepRsaKeySize
6263
from .semgrep.semgrep_sql_parametrization import SemgrepSQLParameterization
6364
from .semgrep.semgrep_subprocess_shell_false import SemgrepSubprocessShellFalse
@@ -207,6 +208,7 @@
207208
codemods=[
208209
SemgrepUrlSandbox,
209210
SemgrepEnableJinja2Autoescape,
211+
SemgrepNoCsrfExempt,
210212
SemgrepJwtDecodeVerify,
211213
SemgrepUseDefusedXml,
212214
SemgrepSubprocessShellFalse,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import libcst as cst
2+
3+
from codemodder.codemods.base_codemod import (
4+
Metadata,
5+
ReviewGuidance,
6+
ToolMetadata,
7+
ToolRule,
8+
)
9+
from codemodder.codemods.libcst_transformer import (
10+
LibcstResultTransformer,
11+
LibcstTransformerPipeline,
12+
)
13+
from codemodder.codemods.semgrep import SemgrepSarifFileDetector
14+
from codemodder.codemods.utils_mixin import NameResolutionMixin
15+
from core_codemods.semgrep.api import SemgrepCodemod, semgrep_url_from_id
16+
17+
18+
class RemoveCsrfExemptTransformer(LibcstResultTransformer, NameResolutionMixin):
19+
change_description = "Remove `@csrf_exempt` decorator from Django view"
20+
21+
def leave_Decorator(
22+
self, original_node: cst.Decorator, updated_node: cst.Decorator
23+
):
24+
if not self.filter_by_path_includes_or_excludes(
25+
self.node_position(original_node)
26+
):
27+
return updated_node
28+
29+
if (
30+
self.find_base_name(original_node.decorator)
31+
== "django.views.decorators.csrf.csrf_exempt"
32+
):
33+
self.report_change(original_node)
34+
return cst.RemovalSentinel.REMOVE
35+
return original_node
36+
37+
38+
SemgrepNoCsrfExempt = SemgrepCodemod(
39+
metadata=Metadata(
40+
name="no-csrf-exempt",
41+
summary=RemoveCsrfExemptTransformer.change_description.title(),
42+
description=RemoveCsrfExemptTransformer.change_description.title(),
43+
review_guidance=ReviewGuidance.MERGE_AFTER_REVIEW,
44+
tool=ToolMetadata(
45+
name="Semgrep",
46+
rules=[
47+
ToolRule(
48+
id=(
49+
rule_id := "python.django.security.audit.csrf-exempt.no-csrf-exempt"
50+
),
51+
name="no-csrf-exempt",
52+
url=semgrep_url_from_id(rule_id),
53+
)
54+
],
55+
),
56+
references=[],
57+
),
58+
transformer=LibcstTransformerPipeline(RemoveCsrfExemptTransformer),
59+
detector=SemgrepSarifFileDetector(),
60+
requested_rules=[rule_id],
61+
)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import json
2+
3+
from codemodder.codemods.test import BaseSASTCodemodTest
4+
from core_codemods.semgrep.semgrep_no_csrf_exempt import SemgrepNoCsrfExempt
5+
6+
7+
class TestSemgrepNoCsrfExempt(BaseSASTCodemodTest):
8+
codemod = SemgrepNoCsrfExempt
9+
tool = "semgrep"
10+
11+
def test_name(self):
12+
assert self.codemod.name == "no-csrf-exempt"
13+
14+
def test_decorators(self, tmpdir):
15+
input_code = """\
16+
from django.http import JsonResponse
17+
from django.views.decorators.csrf import csrf_exempt
18+
from django.dispatch import receiver
19+
from django.core.signals import request_finished
20+
21+
@csrf_exempt
22+
def ssrf_code_checker(request):
23+
if request.user.is_authenticated:
24+
if request.method == 'POST':
25+
return JsonResponse({'message': 'Testbench failed'}, status=200)
26+
return JsonResponse({'message': 'UnAuthenticated User'}, status=401)
27+
28+
29+
@receiver(request_finished)
30+
@csrf_exempt
31+
def foo():
32+
pass
33+
"""
34+
expected_output = """\
35+
from django.http import JsonResponse
36+
from django.views.decorators.csrf import csrf_exempt
37+
from django.dispatch import receiver
38+
from django.core.signals import request_finished
39+
40+
def ssrf_code_checker(request):
41+
if request.user.is_authenticated:
42+
if request.method == 'POST':
43+
return JsonResponse({'message': 'Testbench failed'}, status=200)
44+
return JsonResponse({'message': 'UnAuthenticated User'}, status=401)
45+
46+
47+
@receiver(request_finished)
48+
def foo():
49+
pass
50+
"""
51+
52+
results = {
53+
"runs": [
54+
{
55+
"results": [
56+
{
57+
"fingerprints": {"matchBasedId/v1": "a3ca2"},
58+
"locations": [
59+
{
60+
"physicalLocation": {
61+
"artifactLocation": {
62+
"uri": "code.py",
63+
"uriBaseId": "%SRCROOT%",
64+
},
65+
"region": {
66+
"endColumn": 73,
67+
"endLine": 11,
68+
"snippet": {
69+
"text": "@csrf_exempt\ndef ssrf_code_checker(request):\n if request.user.is_authenticated:\n if request.method == 'POST':\n return JsonResponse({'message': 'Testbench failed'}, status=200)\n return JsonResponse({'message': 'UnAuthenticated User'}, status=401)"
70+
},
71+
"startColumn": 1,
72+
"startLine": 6,
73+
},
74+
}
75+
}
76+
],
77+
"message": {
78+
"text": "Detected usage of @csrf_exempt, which indicates that there is no CSRF token set for this route. This could lead to an attacker manipulating the user's account and exfiltration of private data. Instead, create a function without this decorator."
79+
},
80+
"ruleId": "python.django.security.audit.csrf-exempt.no-csrf-exempt",
81+
},
82+
{
83+
"fingerprints": {"matchBasedId/v1": "1cc62"},
84+
"locations": [
85+
{
86+
"physicalLocation": {
87+
"artifactLocation": {
88+
"uri": "code.py",
89+
"uriBaseId": "%SRCROOT%",
90+
},
91+
"region": {
92+
"endColumn": 9,
93+
"endLine": 17,
94+
"snippet": {
95+
"text": "@receiver(request_finished)\n@csrf_exempt\ndef foo():\n pass"
96+
},
97+
"startColumn": 1,
98+
"startLine": 14,
99+
},
100+
}
101+
}
102+
],
103+
"message": {
104+
"text": "Detected usage of @csrf_exempt, which indicates that there is no CSRF token set for this route. This could lead to an attacker manipulating the user's account and exfiltration of private data. Instead, create a function without this decorator."
105+
},
106+
"ruleId": "python.django.security.audit.csrf-exempt.no-csrf-exempt",
107+
},
108+
],
109+
}
110+
]
111+
}
112+
self.run_and_assert(
113+
tmpdir,
114+
input_code,
115+
expected_output,
116+
results=json.dumps(results),
117+
num_changes=2,
118+
)

0 commit comments

Comments
 (0)