Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2648962

Browse files
authored
↪️ Merge pull request #239 from gdemarcsek/f/jwt-plugin
JWT Detector Plugin
2 parents 7be2b80 + c54dcd6 commit 2648962

9 files changed

Lines changed: 126 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ The current heuristic searches we implement out of the box include:
162162

163163
* **RegexBasedDetector**: checks for any keys matching certain regular expressions (Artifactory, AWS, Slack, Stripe, Mailchimp).
164164

165+
**JwtTokenDetector**: checks for formally correct JWTs.
166+
165167
See [detect_secrets/
166168
plugins](https://github.com/Yelp/detect-secrets/tree/master/detect_secrets/plugins)
167169
for more details.

detect_secrets/core/usage.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ class PluginOptions(object):
335335
disable_flag_text='--no-mailchimp-scan',
336336
disable_help_text='Disable scanning for Mailchimp keys',
337337
),
338+
PluginDescriptor(
339+
classname='JwtTokenDetector',
340+
disable_flag_text='--no-jwt-scan',
341+
disable_help_text='Disable scanning for JWTs',
342+
),
338343
]
339344

340345
def __init__(self, parser):

detect_secrets/plugins/common/initialize.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ..common.util import get_mapping_from_secret_type_to_class_name
77
from ..high_entropy_strings import Base64HighEntropyString # noqa: F401
88
from ..high_entropy_strings import HexHighEntropyString # noqa: F401
9+
from ..jwt import JwtTokenDetector # noqa: F401
910
from ..keyword import KeywordDetector # noqa: F401
1011
from ..mailchimp import MailchimpDetector # noqa: F401
1112
from ..private_key import PrivateKeyDetector # noqa: F401

detect_secrets/plugins/common/util.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ..basic_auth import BasicAuthDetector # noqa: F401
1212
from ..high_entropy_strings import Base64HighEntropyString # noqa: F401
1313
from ..high_entropy_strings import HexHighEntropyString # noqa: F401
14+
from ..jwt import JwtTokenDetector # noqa: F401
1415
from ..keyword import KeywordDetector # noqa: F401
1516
from ..private_key import PrivateKeyDetector # noqa: F401
1617
from ..slack import SlackDetector # noqa: F401

detect_secrets/plugins/jwt.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
This plugin finds JWT tokens
3+
"""
4+
from __future__ import absolute_import
5+
6+
import base64
7+
import json
8+
import re
9+
10+
from .base import RegexBasedDetector
11+
12+
try:
13+
# Python 2
14+
from future_builtins import filter
15+
except ImportError:
16+
# Python 3
17+
pass
18+
19+
20+
class JwtTokenDetector(RegexBasedDetector):
21+
secret_type = 'JSON Web Token'
22+
denylist = [
23+
re.compile(r'eyJ[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*?'),
24+
]
25+
26+
def secret_generator(self, string, *args, **kwargs):
27+
return filter(
28+
self.is_formally_valid,
29+
super(JwtTokenDetector, self).secret_generator(string, *args, **kwargs),
30+
)
31+
32+
@staticmethod
33+
def is_formally_valid(token):
34+
parts = token.split('.')
35+
for idx, part in enumerate(parts):
36+
try:
37+
part = part.encode('ascii')
38+
# https://github.com/magical/jwt-python/blob/2fd976b41111031313107792b40d5cfd1a8baf90/jwt.py#L49
39+
# https://github.com/jpadilla/pyjwt/blob/3d47b0ea9e5d489f9c90ee6dde9e3d9d69244e3a/jwt/utils.py#L33
40+
m = len(part) % 4
41+
if m == 1:
42+
raise TypeError('Incorrect padding')
43+
elif m == 2:
44+
part += '=='.encode('utf-8')
45+
elif m == 3:
46+
part += '==='.encode('utf-8')
47+
b64_decoded = base64.urlsafe_b64decode(part)
48+
if idx < 2:
49+
_ = json.loads(b64_decoded.decode('utf-8'))
50+
except (TypeError, ValueError, UnicodeDecodeError):
51+
return False
52+
53+
return True

tests/core/usage_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def test_consolidates_output_basic(self):
4242
'ArtifactoryDetector': {},
4343
'StripeDetector': {},
4444
'MailchimpDetector': {},
45+
'JwtTokenDetector': {},
4546
}
4647
assert not hasattr(args, 'no_private_key_scan')
4748

tests/main_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def test_scan_string_basic(
9494
Base64HighEntropyString: {}
9595
BasicAuthDetector : False
9696
HexHighEntropyString : {}
97+
JwtTokenDetector : False
9798
KeywordDetector : False
9899
MailchimpDetector : False
99100
PrivateKeyDetector : False
@@ -120,6 +121,7 @@ def test_scan_string_cli_overrides_stdin(self):
120121
Base64HighEntropyString: False (2.585)
121122
BasicAuthDetector : False
122123
HexHighEntropyString : False (2.121)
124+
JwtTokenDetector : False
123125
KeywordDetector : False
124126
MailchimpDetector : False
125127
PrivateKeyDetector : False
@@ -254,6 +256,9 @@ def test_old_baseline_ignored_with_update_flag(
254256
'hex_limit': 3,
255257
'name': 'HexHighEntropyString',
256258
},
259+
{
260+
'name': 'JwtTokenDetector',
261+
},
257262
{
258263
'name': 'KeywordDetector',
259264
},
@@ -294,6 +299,9 @@ def test_old_baseline_ignored_with_update_flag(
294299
'hex_limit': 3,
295300
'name': 'HexHighEntropyString',
296301
},
302+
{
303+
'name': 'JwtTokenDetector',
304+
},
297305
{
298306
'name': 'KeywordDetector',
299307
},
@@ -387,6 +395,9 @@ def test_old_baseline_ignored_with_update_flag(
387395
{
388396
'name': 'BasicAuthDetector',
389397
},
398+
{
399+
'name': 'JwtTokenDetector',
400+
},
390401
{
391402
'name': 'MailchimpDetector',
392403
},
@@ -426,6 +437,9 @@ def test_old_baseline_ignored_with_update_flag(
426437
{
427438
'name': 'BasicAuthDetector',
428439
},
440+
{
441+
'name': 'JwtTokenDetector',
442+
},
429443
{
430444
'name': 'MailchimpDetector',
431445
},

tests/plugins/jwt_test.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from __future__ import absolute_import
2+
3+
import pytest
4+
5+
from detect_secrets.plugins.jwt import JwtTokenDetector
6+
7+
8+
class TestJwtTokenDetector(object):
9+
10+
@pytest.mark.parametrize(
11+
'payload, should_flag',
12+
[
13+
# valid jwt
14+
('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', True), # noqa: E501
15+
# valid jwt - but header contains CR/LF-s
16+
('eyJ0eXAiOiJKV1QiLA0KImFsZyI6IkhTMjU2In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ', True), # noqa: E501
17+
# valid jwt - but claims contain bunch of LF newlines
18+
('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9lIiwKInN0YXR1cyI6ImVtcGxveWVlIgp9', True), # noqa: E501
19+
# valid jwt - claims contain strings with unicode accents
20+
('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IsWww6HFkcOtIMOWxZHDqcOoIiwiaWF0IjoxNTE2MjM5MDIyfQ.k5HibI_uLn_RTuPcaCNkaVaQH2y5q6GvJg8GPpGMRwQ', True), # noqa: E501
21+
# as unicode literal
22+
(u'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', True), # noqa: E501
23+
# no signature - but still valid
24+
('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ', True), # noqa: E501
25+
# decoded - invalid
26+
('{"alg":"HS256","typ":"JWT"}.{"name":"Jon Doe"}.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', False), # noqa: E501
27+
# invalid json - invalid (caught by regex)
28+
('bm90X3ZhbGlkX2pzb25fYXRfYWxs.bm90X3ZhbGlkX2pzb25fYXRfYWxs.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', False), # noqa: E501
29+
# missing claims - invalid
30+
('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', False), # noqa: E501
31+
# totally not a jwt
32+
('jwt', False), # noqa: E501
33+
# invalid json with random bytes
34+
('eyJhbasdGciOiJIUaddasdasfsasdasdzI1NiIasdsInR5cCI6IkpXVCasdJasd9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', False), # noqa: E501
35+
# invalid json in jwt header - invalid (caught by parsing)
36+
('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', False), # noqa: E501
37+
# good by regex, but otherwise totally not JWT
38+
('eyJAAAA.eyJBBB', False), # noqa: E501
39+
('eyJBB.eyJCC.eyJDDDD', False), # noqa: E501
40+
],
41+
)
42+
def test_analyze_string(self, payload, should_flag):
43+
logic = JwtTokenDetector()
44+
45+
output = logic.analyze_string(payload, 1, 'mock_filename')
46+
assert len(output) == int(should_flag)

tests/pre_commit_hook_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ def test_that_baseline_gets_updated(
191191
'hex_limit': 3,
192192
'name': 'HexHighEntropyString',
193193
},
194+
{
195+
'name': 'JwtTokenDetector',
196+
},
194197
{
195198
'name': 'KeywordDetector',
196199
},

0 commit comments

Comments
 (0)