Skip to content

Commit 0b5ad52

Browse files
authored
Semgrep jwt decode verify (#696)
* new semgrep jwt decode codemod * refactor SAST Transformer * remove -semgrep name prefix * fix import
1 parent 804da0d commit 0b5ad52

File tree

9 files changed

+117
-38
lines changed

9 files changed

+117
-38
lines changed

integration_tests/sonar/test_sonar_jwt_decode_verify.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 SonarIntegrationTest
22
from core_codemods.sonar.sonar_jwt_decode_verify import (
3-
JwtDecodeVerifySonarTransformer,
3+
JwtDecodeVerifySASTTransformer,
44
SonarJwtDecodeVerify,
55
)
66

@@ -22,4 +22,4 @@ class TestJwtDecodeVerify(SonarIntegrationTest):
2222
expected_diff = '--- \n+++ \n@@ -8,7 +8,7 @@\n \n encoded_jwt = jwt.encode(payload, SECRET_KEY, algorithm="HS256")\n \n-decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=False)\n-decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": False})\n+decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=True)\n+decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": True})\n \n var = "something"\n'
2323
expected_line_change = "11"
2424
num_changes = 2
25-
change_description = JwtDecodeVerifySonarTransformer.change_description
25+
change_description = JwtDecodeVerifySASTTransformer.change_description

src/codemodder/scripts/generate_docs.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,14 +323,13 @@ class DocMetadata:
323323
}
324324

325325
SEMGREP_CODEMOD_NAMES = [
326-
"enable-jinja2-autoescape-semgrep",
326+
"enable-jinja2-autoescape",
327+
"jwt-decode-verify",
327328
]
328329
SEMGREP_CODEMODS = {
329330
name: DocMetadata(
330-
importance=CORE_CODEMODS[
331-
core_codemod_name := "-".join(name.split("-")[:-1])
332-
].importance,
333-
guidance_explained=CORE_CODEMODS[core_codemod_name].guidance_explained,
331+
importance=CORE_CODEMODS[name].importance,
332+
guidance_explained=CORE_CODEMODS[name].guidance_explained,
334333
need_sarif="Yes (Semgrep)",
335334
)
336335
for name in SEMGREP_CODEMOD_NAMES

src/core_codemods/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from .secure_flask_session_config import SecureFlaskSessionConfig
5656
from .secure_random import SecureRandom
5757
from .semgrep.semgrep_enable_jinja2_autoescape import SemgrepEnableJinja2Autoescape
58+
from .semgrep.semgrep_jwt_decode_verify import SemgrepJwtDecodeVerify
5859
from .sonar.sonar_break_or_continue_out_of_loop import SonarBreakOrContinueOutOfLoop
5960
from .sonar.sonar_disable_graphql_introspection import SonarDisableGraphQLIntrospection
6061
from .sonar.sonar_django_json_response_type import SonarDjangoJsonResponseType
@@ -198,5 +199,6 @@
198199
origin="semgrep",
199200
codemods=[
200201
SemgrepEnableJinja2Autoescape,
202+
SemgrepJwtDecodeVerify,
201203
],
202204
)

src/core_codemods/jwt_decode_verify.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
NewArg,
88
)
99
from codemodder.codemods.semgrep import SemgrepRuleDetector
10+
from codemodder.result import fuzzy_column_match, same_line
1011
from core_codemods.api import Metadata, Reference, ReviewGuidance
1112
from core_codemods.api.core_codemod import CoreCodemod
1213

@@ -58,6 +59,29 @@ def on_result_found(self, original_node, updated_node):
5859
return self.update_arg_target(updated_node, new_args)
5960

6061

62+
class JwtDecodeVerifySASTTransformer(JwtDecodeVerifyTransformer):
63+
def filter_by_result(self, node) -> bool:
64+
"""
65+
Special case result-matching for this rule because the SAST
66+
results returned have a start/end column for the verify keyword
67+
within the `decode` call, not for the entire `decode` call.
68+
"""
69+
match node:
70+
case cst.Call():
71+
pos_to_match = self.node_position(node)
72+
return any(
73+
self.match_location(pos_to_match, result)
74+
for result in self.results or []
75+
)
76+
return False
77+
78+
def match_location(self, pos, result):
79+
return any(
80+
same_line(pos, location) and fuzzy_column_match(pos, location)
81+
for location in result.locations
82+
)
83+
84+
6185
def is_verify_keyword(element: cst.DictElement) -> bool:
6286
"""Determine if DictElement is something like:
6387
DictElement(

src/core_codemods/semgrep/semgrep_enable_jinja2_autoescape.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from core_codemods.semgrep.api import SemgrepCodemod
33

44
SemgrepEnableJinja2Autoescape = SemgrepCodemod.from_core_codemod(
5-
name="enable-jinja2-autoescape-semgrep",
5+
name="enable-jinja2-autoescape",
66
other=EnableJinja2Autoescape,
77
rule_id="python.flask.security.xss.audit.direct-use-of-jinja2.direct-use-of-jinja2",
88
rule_name="direct-use-of-jinja2",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from codemodder.codemods.libcst_transformer import LibcstTransformerPipeline
2+
from core_codemods.jwt_decode_verify import (
3+
JwtDecodeVerify,
4+
JwtDecodeVerifySASTTransformer,
5+
)
6+
from core_codemods.semgrep.api import SemgrepCodemod
7+
8+
SemgrepJwtDecodeVerify = SemgrepCodemod.from_core_codemod(
9+
name="jwt-decode-verify",
10+
other=JwtDecodeVerify,
11+
rule_id="python.jwt.security.unverified-jwt-decode.unverified-jwt-decode",
12+
rule_name="unverified-jwt-decode",
13+
transformer=LibcstTransformerPipeline(JwtDecodeVerifySASTTransformer),
14+
)
Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,14 @@
1-
import libcst as cst
2-
31
from codemodder.codemods.libcst_transformer import LibcstTransformerPipeline
4-
from codemodder.result import fuzzy_column_match, same_line
5-
from core_codemods.jwt_decode_verify import JwtDecodeVerify, JwtDecodeVerifyTransformer
2+
from core_codemods.jwt_decode_verify import (
3+
JwtDecodeVerify,
4+
JwtDecodeVerifySASTTransformer,
5+
)
66
from core_codemods.sonar.api import SonarCodemod
77

8-
9-
class JwtDecodeVerifySonarTransformer(JwtDecodeVerifyTransformer):
10-
def filter_by_result(self, node) -> bool:
11-
"""
12-
Special case result-matching for this rule because the sonar
13-
results returned have a start/end column for the verify keyword
14-
within the `decode` call, not for the entire call like semgrep returns.
15-
"""
16-
match node:
17-
case cst.Call():
18-
pos_to_match = self.node_position(node)
19-
return any(
20-
self.match_location(pos_to_match, result)
21-
for result in self.results or []
22-
)
23-
return False
24-
25-
def match_location(self, pos, result):
26-
return any(
27-
same_line(pos, location) and fuzzy_column_match(pos, location)
28-
for location in result.locations
29-
)
30-
31-
328
SonarJwtDecodeVerify = SonarCodemod.from_core_codemod(
339
name="jwt-decode-verify-S5659",
3410
other=JwtDecodeVerify,
3511
rule_id="python:S5659",
3612
rule_name="JWT should be signed and verified",
37-
transformer=LibcstTransformerPipeline(JwtDecodeVerifySonarTransformer),
13+
transformer=LibcstTransformerPipeline(JwtDecodeVerifySASTTransformer),
3814
)

tests/codemods/semgrep/test_semgrep_enable_jinja2_autoescape.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class TestEnableJinja2Autoescape(BaseSASTCodemodTest):
1111
tool = "semgrep"
1212

1313
def test_name(self):
14-
assert self.codemod.name == "enable-jinja2-autoescape-semgrep"
14+
assert self.codemod.name == "enable-jinja2-autoescape"
1515

1616
def test_import(self, tmpdir):
1717
input_code = """
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import json
2+
3+
from codemodder.codemods.test import BaseSASTCodemodTest
4+
from core_codemods.semgrep.semgrep_jwt_decode_verify import SemgrepJwtDecodeVerify
5+
6+
7+
class TestSemgrepJwtDecodeVerify(BaseSASTCodemodTest):
8+
codemod = SemgrepJwtDecodeVerify
9+
tool = "semgrep"
10+
11+
def test_name(self):
12+
assert self.codemod.name == "jwt-decode-verify"
13+
14+
def test_import(self, tmpdir):
15+
input_code = """
16+
import jwt
17+
18+
jwt.decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], options={"verify_signature": False})
19+
"""
20+
expected_output = """
21+
import jwt
22+
23+
jwt.decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], options={"verify_signature": True})
24+
"""
25+
results = {
26+
"runs": [
27+
{
28+
"results": [
29+
{
30+
"fingerprints": {"matchBasedId/v1": "123"},
31+
"locations": [
32+
{
33+
"physicalLocation": {
34+
"artifactLocation": {
35+
"uri": "code.py",
36+
"uriBaseId": "%SRCROOT%",
37+
},
38+
"region": {
39+
"endColumn": 93,
40+
"endLine": 4,
41+
"snippet": {
42+
"text": "jwt.decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], options={\"verify_signature\": False})"
43+
},
44+
"startColumn": 88,
45+
"startLine": 4,
46+
},
47+
}
48+
}
49+
],
50+
"message": {
51+
"text": "Detected JWT token decoded with 'verify=False'. This bypasses any integrity checks for the token which means the token could be tampered with by malicious actors. Ensure that the JWT token is verified."
52+
},
53+
"ruleId": "python.jwt.security.unverified-jwt-decode.unverified-jwt-decode",
54+
}
55+
]
56+
}
57+
]
58+
}
59+
self.run_and_assert(
60+
tmpdir,
61+
input_code,
62+
expected_output,
63+
results=json.dumps(results),
64+
)

0 commit comments

Comments
 (0)