Skip to content

Commit e9dab50

Browse files
authored
new jwt decode verify codemod (pixee#27)
new jwt decode verify codemod
1 parent 4c8c37d commit e9dab50

8 files changed

Lines changed: 289 additions & 1 deletion

File tree

‎.github/workflows/lint.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: Install Dependencies
3232
run: pip install -r requirements/lint.txt
3333
- name: Black Format Check
34-
run: black --check .
34+
run: black --check . --exclude samples/
3535
- name: Run pylint
3636
run: make lint
3737

‎.pre-commit-config.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ repos:
1111
rev: 23.9.1
1212
hooks:
1313
- id: black
14+
exclude: samples/
1415
- repo: https://github.com/pre-commit/mirrors-mypy
1516
rev: v1.5.1
1617
hooks:
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from codemodder.codemods.jwt_decode_verify import JwtDecodeVerify
2+
from integration_tests.base_test import (
3+
BaseIntegrationTest,
4+
original_and_expected_from_code_path,
5+
)
6+
7+
8+
class TestJwtDecodeVerify(BaseIntegrationTest):
9+
codemod = JwtDecodeVerify
10+
code_path = "tests/samples/jwt_decode_verify.py"
11+
original_code, expected_new_code = original_and_expected_from_code_path(
12+
code_path,
13+
[
14+
(
15+
10,
16+
"""decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=True)\n""",
17+
),
18+
(
19+
11,
20+
"""decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": True})\n""",
21+
),
22+
],
23+
)
24+
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'
25+
expected_line_change = "11"
26+
num_changes = 2
27+
change_description = JwtDecodeVerify.CHANGE_DESCRIPTION

‎src/codemodder/codemods/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from codemodder.codemods.tempfile_mktemp import TempfileMktemp
2020
from codemodder.codemods.requests_verify import RequestsVerify
2121
from codemodder.codemods.use_walrus_if import UseWalrusIf
22+
from codemodder.codemods.jwt_decode_verify import JwtDecodeVerify
2223

2324
DEFAULT_CODEMODS = [
2425
DjangoDebugFlagOn,
@@ -38,6 +39,7 @@
3839
TempfileMktemp,
3940
RequestsVerify,
4041
UseWalrusIf,
42+
JwtDecodeVerify,
4143
]
4244
ALL_CODEMODS = DEFAULT_CODEMODS
4345

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
This codemod ensures calls to [jwt.decode](https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode) do not disable signature validation and other
2+
verifications. It checks that both the `verify` parameter (soon to be deprecated) and any `verify` key in the `options` dict parameter are not assigned to `False`.
3+
4+
Our change looks as follows:
5+
6+
```diff
7+
import jwt
8+
...
9+
- decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=False)
10+
+ decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=True)
11+
...
12+
- decoded_payload = jwt.decode(
13+
encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": False, "verify_exp": False})
14+
+ decoded_payload = jwt.decode(
15+
encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": True, "verify_exp": True})
16+
```
17+
18+
Any `verify` parameter not listed relies on the secure `True` default value.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import libcst as cst
2+
from libcst import matchers
3+
from codemodder.codemods.base_codemod import ReviewGuidance
4+
from codemodder.codemods.api import SemgrepCodemod
5+
6+
7+
class JwtDecodeVerify(SemgrepCodemod):
8+
NAME = "jwt-decode-verify"
9+
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW
10+
SUMMARY = "Enable all verifications in `jwt.decode` call."
11+
DESCRIPTION = "Makes any of the multiple `verify` parameters to a `jwt.decode` call be `True`."
12+
13+
@classmethod
14+
def rule(cls):
15+
return r"""
16+
rules:
17+
- pattern-either:
18+
- patterns:
19+
- pattern: jwt.decode(..., verify=False, ...)
20+
- pattern-inside: |
21+
import jwt
22+
...
23+
- patterns:
24+
- pattern: |
25+
jwt.decode(..., options={..., "$KEY": False, ...}, ...)
26+
- metavariable-regex:
27+
metavariable: $KEY
28+
regex: verify_
29+
- pattern-inside: |
30+
import jwt
31+
...
32+
"""
33+
34+
def _replace_opts_dict(self, opts_dict):
35+
new_dict_elements = []
36+
37+
for element in opts_dict.elements:
38+
if is_verify_keyword(element):
39+
new_el = cst.DictElement(
40+
key=cst.parse_expression(element.key.value),
41+
value=cst.parse_expression("True"),
42+
)
43+
else:
44+
new_el = element
45+
new_dict_elements.append(new_el)
46+
return new_dict_elements
47+
48+
def replace_options_arg(self, node_args):
49+
new_args = []
50+
for arg in node_args:
51+
if matchers.matches(arg.keyword, matchers.Name("options")) and isinstance(
52+
opts_dict := arg.value, cst.Dict
53+
):
54+
new_dict_elements = self._replace_opts_dict(opts_dict)
55+
new = cst.Arg(
56+
keyword=cst.parse_expression("options"),
57+
value=cst.Dict(
58+
elements=new_dict_elements,
59+
),
60+
equal=arg.equal,
61+
)
62+
else:
63+
new = arg
64+
new_args.append(new)
65+
return new_args
66+
67+
def replace_arg(self, original_node, target_arg_name, target_arg_replacement_val):
68+
new_args = super().replace_arg(original_node, "verify", "True")
69+
return self.replace_options_arg(new_args)
70+
71+
def on_result_found(self, original_node, updated_node):
72+
new_args = self.replace_arg(original_node, "verify", "True")
73+
return self.update_arg_target(updated_node, new_args)
74+
75+
76+
def is_verify_keyword(element: cst.DictElement) -> bool:
77+
"""Determine if DictElement is something like:
78+
DictElement(
79+
key=SimpleString(
80+
value='"verify_signature"',
81+
lpar=[],
82+
rpar=[],
83+
)
84+
...
85+
where value should be anything with the word `verify`
86+
"""
87+
return (
88+
matchers.matches(element.key, matchers.SimpleString())
89+
and "verify" in element.key.value
90+
)
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import pytest
2+
from codemodder.codemods.jwt_decode_verify import JwtDecodeVerify
3+
from tests.codemods.base_codemod_test import BaseSemgrepCodemodTest
4+
5+
6+
class TestJwtDecodeVerify(BaseSemgrepCodemodTest):
7+
codemod = JwtDecodeVerify
8+
9+
def test_rule_ids(self):
10+
assert self.codemod.RULE_IDS == ["jwt-decode-verify"]
11+
12+
def test_import(self, tmpdir):
13+
input_code = """import jwt
14+
15+
jwt.decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], verify=False)
16+
var = "hello"
17+
"""
18+
expexted_output = """import jwt
19+
20+
jwt.decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], verify=True)
21+
var = "hello"
22+
"""
23+
24+
self.run_and_assert(tmpdir, input_code, expexted_output)
25+
26+
def test_from_import(self, tmpdir):
27+
input_code = """from jwt import decode
28+
29+
decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], verify=False)
30+
var = "hello"
31+
"""
32+
expexted_output = """from jwt import decode
33+
34+
decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], verify=True)
35+
var = "hello"
36+
"""
37+
38+
self.run_and_assert(tmpdir, input_code, expexted_output)
39+
40+
def test_import_alias(self, tmpdir):
41+
input_code = """import jwt as _jwtmod
42+
43+
_jwtmod.decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], verify=False)
44+
var = "hello"
45+
"""
46+
expexted_output = """import jwt as _jwtmod
47+
48+
_jwtmod.decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], verify=True)
49+
var = "hello"
50+
"""
51+
52+
self.run_and_assert(tmpdir, input_code, expexted_output)
53+
54+
@pytest.mark.parametrize(
55+
"input_args,expected_args",
56+
[
57+
(
58+
"",
59+
"",
60+
),
61+
(
62+
"verify=True",
63+
"verify=True",
64+
),
65+
(
66+
"""verify=False, options={"verify_signature": False}""",
67+
"""verify=True, options={"verify_signature": True}""",
68+
),
69+
(
70+
"""options={"verify_exp": False}""",
71+
"""options={"verify_exp": True}""",
72+
),
73+
(
74+
"""options={"verify_iss": False, "verify_exp": False, }""",
75+
"""options={"verify_iss": True, "verify_exp": True}""",
76+
),
77+
(
78+
"""options={"strict_aud": False, "verify_signature": False}""",
79+
"""options={"strict_aud": False, "verify_signature": True}""",
80+
),
81+
(
82+
"""options={"verify_iss": True, "verify_signature": True}""",
83+
"""options={"verify_iss": True, "verify_signature": True}""",
84+
),
85+
],
86+
)
87+
def test_verify_variations(self, tmpdir, input_args, expected_args):
88+
input_code = f"""import jwt
89+
90+
jwt.decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], {input_args})
91+
var = "hello"
92+
"""
93+
expexted_output = f"""import jwt
94+
95+
jwt.decode(encoded_jwt, SECRET_KEY, algorithms=['HS256'], {expected_args})
96+
var = "hello"
97+
"""
98+
99+
self.run_and_assert(tmpdir, input_code, expexted_output)
100+
101+
def test_multiline_formatting_verify_flag(self, tmpdir):
102+
input_code = """import jwt
103+
104+
decoded_payload = jwt.decode(
105+
encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=False
106+
)
107+
var = "hello"
108+
"""
109+
expexted_output = """import jwt
110+
111+
decoded_payload = jwt.decode(
112+
encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=True)
113+
var = "hello"
114+
"""
115+
116+
self.run_and_assert(tmpdir, input_code, expexted_output)
117+
118+
@pytest.mark.parametrize("quote", ["'", '"'])
119+
def test_multiline_formatting_options(self, tmpdir, quote):
120+
input_code = f"""import jwt
121+
122+
decoded_payload = jwt.decode(
123+
encoded_jwt, SECRET_KEY, algorithms=["HS256"],
124+
options={{{quote}verify_signature{quote}: False, {quote}verify_exp{quote}: False}}
125+
)
126+
var = "hello"
127+
"""
128+
expexted_output = f"""import jwt
129+
130+
decoded_payload = jwt.decode(
131+
encoded_jwt, SECRET_KEY, algorithms=["HS256"],
132+
options={{{quote}verify_signature{quote}: True, {quote}verify_exp{quote}: True}})
133+
var = "hello"
134+
"""
135+
136+
self.run_and_assert(tmpdir, input_code, expexted_output)

‎tests/samples/jwt_decode_verify.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import jwt
2+
3+
SECRET_KEY = "mysecretkey"
4+
payload = {
5+
"user_id": 123,
6+
"username": "john",
7+
}
8+
9+
encoded_jwt = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
10+
11+
decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=False)
12+
decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": False})
13+
14+
var = "something"

0 commit comments

Comments
 (0)