File CVE-2024-33663.patch of Package python-python-jose
600
1
From 34bd82c43ea31da5b9deaa25ff591905a180bdf7 Mon Sep 17 00:00:00 2001
2
From: Daniel Garcia Moreno <daniel.garcia@suse.com>
3
Date: Thu, 2 May 2024 09:29:54 +0200
4
Subject: [PATCH 1/4] Improve asymmetric key check in CryptographyHMACKey
5
6
This change should fix https://github.com/mpdavis/python-jose/issues/346
7
security issue.
8
9
The code is based on pyjwt change:
10
https://github.com/jpadilla/pyjwt/commit/9c528670c455b8d948aff95ed50e22940d1ad3fc
11
---
12
jose/backends/cryptography_backend.py | 72 ++++++++++++++++++++++++---
13
tests/test_jwt.py | 35 ++++++++++++-
14
2 files changed, 98 insertions(+), 9 deletions(-)
15
16
Index: python-jose-3.3.0/jose/backends/cryptography_backend.py
17
===================================================================
18
--- python-jose-3.3.0.orig/jose/backends/cryptography_backend.py
19
+++ python-jose-3.3.0/jose/backends/cryptography_backend.py
20
21
from ..constants import ALGORITHMS
22
from ..exceptions import JWEError, JWKError
23
from ..utils import base64_to_long, base64url_decode, base64url_encode, ensure_binary, long_to_base64
24
+from ..utils import is_pem_format, is_ssh_key
25
from .base import Key
26
27
_binding = None
28
29
if isinstance(key, str):
30
key = key.encode("utf-8")
31
32
- invalid_strings = [
33
- b"-----BEGIN PUBLIC KEY-----",
34
- b"-----BEGIN RSA PUBLIC KEY-----",
35
- b"-----BEGIN CERTIFICATE-----",
36
- b"ssh-rsa",
37
- ]
38
-
39
- if any(string_value in key for string_value in invalid_strings):
40
+ if is_pem_format(key) or is_ssh_key(key):
41
raise JWKError(
42
"The specified key is an asymmetric key or x509 certificate and"
43
" should not be used as an HMAC secret."
44
Index: python-jose-3.3.0/tests/test_jwt.py
45
===================================================================
46
--- python-jose-3.3.0.orig/tests/test_jwt.py
47
+++ python-jose-3.3.0/tests/test_jwt.py
48
49
import pytest
50
51
from jose import jws, jwt
52
-from jose.exceptions import JWTError
53
+from jose.constants import ALGORITHMS
54
+from jose.exceptions import JWTError, JWKError
55
56
57
@pytest.fixture
58
59
],
60
)
61
def test_numeric_key(self, key, token):
62
- token_info = jwt.decode(token, key)
63
+ token_info = jwt.decode(token, key, algorithms=ALGORITHMS.SUPPORTED)
64
assert token_info == {"name": "test"}
65
66
def test_invalid_claims_json(self):
67
68
69
def test_non_default_headers(self, claims, key, headers):
70
encoded = jwt.encode(claims, key, headers=headers)
71
- decoded = jwt.decode(encoded, key)
72
+ decoded = jwt.decode(encoded, key, algorithms=ALGORITHMS.HS256)
73
assert claims == decoded
74
all_headers = jwt.get_unverified_headers(encoded)
75
for k, v in headers.items():
76
77
78
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJhIjoiYiJ9" ".jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8"
79
80
- decoded = jwt.decode(token, key)
81
+ decoded = jwt.decode(token, key, algorithms=ALGORITHMS.SUPPORTED)
82
83
assert decoded == claims
84
85
86
options = {"leeway": leeway}
87
88
token = jwt.encode(claims, key)
89
- jwt.decode(token, key, options=options)
90
+ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256)
91
92
def test_iat_not_int(self, key):
93
94
95
token = jwt.encode(claims, key)
96
97
with pytest.raises(JWTError):
98
- jwt.decode(token, key)
99
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
100
101
def test_nbf_not_int(self, key):
102
103
104
token = jwt.encode(claims, key)
105
106
with pytest.raises(JWTError):
107
- jwt.decode(token, key)
108
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
109
110
def test_nbf_datetime(self, key):
111
112
113
claims = {"nbf": nbf}
114
115
token = jwt.encode(claims, key)
116
- jwt.decode(token, key)
117
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
118
119
def test_nbf_with_leeway(self, key):
120
121
122
options = {"leeway": 10}
123
124
token = jwt.encode(claims, key)
125
- jwt.decode(token, key, options=options)
126
+ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256)
127
128
def test_nbf_in_future(self, key):
129
130
131
token = jwt.encode(claims, key)
132
133
with pytest.raises(JWTError):
134
- jwt.decode(token, key)
135
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
136
137
def test_nbf_skip(self, key):
138
139
140
token = jwt.encode(claims, key)
141
142
with pytest.raises(JWTError):
143
- jwt.decode(token, key)
144
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
145
146
options = {"verify_nbf": False}
147
148
- jwt.decode(token, key, options=options)
149
+ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256)
150
151
def test_exp_not_int(self, key):
152
153
154
token = jwt.encode(claims, key)
155
156
with pytest.raises(JWTError):
157
- jwt.decode(token, key)
158
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
159
160
def test_exp_datetime(self, key):
161
162
163
claims = {"exp": exp}
164
165
token = jwt.encode(claims, key)
166
- jwt.decode(token, key)
167
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
168
169
def test_exp_with_leeway(self, key):
170
171
172
options = {"leeway": 10}
173
174
token = jwt.encode(claims, key)
175
- jwt.decode(token, key, options=options)
176
+ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256)
177
178
def test_exp_in_past(self, key):
179
180
181
token = jwt.encode(claims, key)
182
183
with pytest.raises(JWTError):
184
- jwt.decode(token, key)
185
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
186
187
def test_exp_skip(self, key):
188
189
190
token = jwt.encode(claims, key)
191
192
with pytest.raises(JWTError):
193
- jwt.decode(token, key)
194
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
195
196
options = {"verify_exp": False}
197
198
- jwt.decode(token, key, options=options)
199
+ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256)
200
201
def test_aud_string(self, key):
202
203
204
claims = {"aud": aud}
205
206
token = jwt.encode(claims, key)
207
- jwt.decode(token, key, audience=aud)
208
+ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256)
209
210
def test_aud_list(self, key):
211
212
213
claims = {"aud": [aud]}
214
215
token = jwt.encode(claims, key)
216
- jwt.decode(token, key, audience=aud)
217
+ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256)
218
219
def test_aud_list_multiple(self, key):
220
221
222
claims = {"aud": [aud, "another"]}
223
224
token = jwt.encode(claims, key)
225
- jwt.decode(token, key, audience=aud)
226
+ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256)
227
228
def test_aud_list_is_strings(self, key):
229
230
231
232
token = jwt.encode(claims, key)
233
with pytest.raises(JWTError):
234
- jwt.decode(token, key, audience=aud)
235
+ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256)
236
237
def test_aud_case_sensitive(self, key):
238
239
240
241
token = jwt.encode(claims, key)
242
with pytest.raises(JWTError):
243
- jwt.decode(token, key, audience="AUDIENCE")
244
+ jwt.decode(token, key, audience="AUDIENCE", algorithms=ALGORITHMS.HS256)
245
246
def test_aud_empty_claim(self, claims, key):
247
248
aud = "audience"
249
250
token = jwt.encode(claims, key)
251
- jwt.decode(token, key, audience=aud)
252
+ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256)
253
254
def test_aud_not_string_or_list(self, key):
255
256
257
258
token = jwt.encode(claims, key)
259
with pytest.raises(JWTError):
260
- jwt.decode(token, key)
261
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
262
263
def test_aud_given_number(self, key):
264
265
266
267
token = jwt.encode(claims, key)
268
with pytest.raises(JWTError):
269
- jwt.decode(token, key, audience=1)
270
+ jwt.decode(token, key, audience=1, algorithms=ALGORITHMS.HS256)
271
272
def test_iss_string(self, key):
273
274
275
claims = {"iss": iss}
276
277
token = jwt.encode(claims, key)
278
- jwt.decode(token, key, issuer=iss)
279
+ jwt.decode(token, key, issuer=iss, algorithms=ALGORITHMS.HS256)
280
281
def test_iss_list(self, key):
282
283
284
claims = {"iss": iss}
285
286
token = jwt.encode(claims, key)
287
- jwt.decode(token, key, issuer=["https://issuer", "issuer"])
288
+ jwt.decode(token, key, issuer=["https://issuer", "issuer"], algorithms=ALGORITHMS.HS256)
289
290
def test_iss_tuple(self, key):
291
292
293
claims = {"iss": iss}
294
295
token = jwt.encode(claims, key)
296
- jwt.decode(token, key, issuer=("https://issuer", "issuer"))
297
+ jwt.decode(token, key, issuer=("https://issuer", "issuer"), algorithms=ALGORITHMS.HS256)
298
299
def test_iss_invalid(self, key):
300
301
302
303
token = jwt.encode(claims, key)
304
with pytest.raises(JWTError):
305
- jwt.decode(token, key, issuer="another")
306
+ jwt.decode(token, key, issuer="another", algorithms=ALGORITHMS.HS256)
307
308
def test_sub_string(self, key):
309
310
311
claims = {"sub": sub}
312
313
token = jwt.encode(claims, key)
314
- jwt.decode(token, key)
315
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
316
317
def test_sub_invalid(self, key):
318
319
320
321
token = jwt.encode(claims, key)
322
with pytest.raises(JWTError):
323
- jwt.decode(token, key)
324
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
325
326
def test_sub_correct(self, key):
327
328
329
claims = {"sub": sub}
330
331
token = jwt.encode(claims, key)
332
- jwt.decode(token, key, subject=sub)
333
+ jwt.decode(token, key, subject=sub, algorithms=ALGORITHMS.HS256)
334
335
def test_sub_incorrect(self, key):
336
337
338
339
token = jwt.encode(claims, key)
340
with pytest.raises(JWTError):
341
- jwt.decode(token, key, subject="another")
342
+ jwt.decode(token, key, subject="another", algorithms=ALGORITHMS.HS256)
343
344
def test_jti_string(self, key):
345
346
347
claims = {"jti": jti}
348
349
token = jwt.encode(claims, key)
350
- jwt.decode(token, key)
351
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
352
353
def test_jti_invalid(self, key):
354
355
356
357
token = jwt.encode(claims, key)
358
with pytest.raises(JWTError):
359
- jwt.decode(token, key)
360
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
361
362
def test_at_hash(self, claims, key):
363
access_token = "<ACCESS_TOKEN>"
364
token = jwt.encode(claims, key, access_token=access_token)
365
- payload = jwt.decode(token, key, access_token=access_token)
366
+ payload = jwt.decode(token, key, access_token=access_token, algorithms=ALGORITHMS.HS256)
367
assert "at_hash" in payload
368
369
def test_at_hash_invalid(self, claims, key):
370
token = jwt.encode(claims, key, access_token="<ACCESS_TOKEN>")
371
with pytest.raises(JWTError):
372
- jwt.decode(token, key, access_token="<OTHER_TOKEN>")
373
+ jwt.decode(token, key, access_token="<OTHER_TOKEN>", algorithms=ALGORITHMS.HS256)
374
375
def test_at_hash_missing_access_token(self, claims, key):
376
token = jwt.encode(claims, key, access_token="<ACCESS_TOKEN>")
377
with pytest.raises(JWTError):
378
- jwt.decode(token, key)
379
+ jwt.decode(token, key, algorithms=ALGORITHMS.HS256)
380
381
def test_at_hash_missing_claim(self, claims, key):
382
token = jwt.encode(claims, key)
383
- payload = jwt.decode(token, key, access_token="<ACCESS_TOKEN>")
384
+ payload = jwt.decode(token, key, access_token="<ACCESS_TOKEN>", algorithms=ALGORITHMS.HS256)
385
assert "at_hash" not in payload
386
387
def test_at_hash_unable_to_calculate(self, claims, key):
388
token = jwt.encode(claims, key, access_token="<ACCESS_TOKEN>")
389
with pytest.raises(JWTError):
390
- jwt.decode(token, key, access_token="\xe2")
391
+ jwt.decode(token, key, access_token="\xe2", algorithms=ALGORITHMS.HS256)
392
393
def test_bad_claims(self):
394
bad_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.iOJ5SiNfaNO_pa2J4Umtb3b3zmk5C18-mhTCVNsjnck"
395
396
397
token = jwt.encode(claims, key)
398
with pytest.raises(JWTError):
399
- jwt.decode(token, key, options=options, audience=str(value))
400
+ jwt.decode(token, key, options=options, audience=str(value), algorithms=ALGORITHMS.HS256)
401
402
new_claims = dict(claims)
403
new_claims[claim] = value
404
token = jwt.encode(new_claims, key)
405
- jwt.decode(token, key, options=options, audience=str(value))
406
+ jwt.decode(token, key, options=options, audience=str(value), algorithms=ALGORITHMS.HS256)
407
+
408
+ def test_CVE_2024_33663(self):
409
+ """Test based on https://github.com/mpdavis/python-jose/issues/346"""
410
+ try:
411
+ from Crypto.PublicKey import ECC
412
+ from Crypto.Hash import HMAC, SHA256
413
+ except ModuleNotFoundError:
414
+ pytest.skip("pycryptodome module not installed")
415
+
416
+ # ----- SETUP -----
417
+ # generate an asymmetric ECC keypair
418
+ # !! signing should only be possible with the private key !!
419
+ KEY = ECC.generate(curve='P-256')
420
+
421
+ # PUBLIC KEY, AVAILABLE TO USER
422
+ # CAN BE RECOVERED THROUGH E.G. PUBKEY RECOVERY WITH TWO SIGNATURES:
423
+ # https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Public_key_recovery
424
+ # https://github.com/FlorianPicca/JWT-Key-Recovery
425
+ PUBKEY = KEY.public_key().export_key(format='OpenSSH').encode()
426
+
427
+ # ---- CLIENT SIDE -----
428
+ # without knowing the private key, a valid token can be constructed
429
+ # YIKES!!
430
+
431
+ b64 = lambda x:base64.urlsafe_b64encode(x).replace(b'=',b'')
432
+ payload = b64(b'{"alg":"HS256"}') + b'.' + b64(b'{"pwned":true}')
433
+ hasher = HMAC.new(PUBKEY, digestmod=SHA256)
434
+ hasher.update(payload)
435
+ evil_token = payload + b'.' + b64(hasher.digest())
436
+
437
+ # ---- SERVER SIDE -----
438
+ # verify and decode the token using the public key, as is custom
439
+ # algorithm field is left unspecified
440
+ # but the library will happily still verify without warning, trusting the user-controlled alg field of the token header
441
+ with pytest.raises(JWKError):
442
+ data = jwt.decode(evil_token, PUBKEY, algorithms=ALGORITHMS.HS256)
443
+
444
+ with pytest.raises(JWTError, match='.*required.*"algorithms".*'):
445
+ data = jwt.decode(evil_token, PUBKEY)
446
Index: python-jose-3.3.0/jose/jwt.py
447
===================================================================
448
--- python-jose-3.3.0.orig/jose/jwt.py
449
+++ python-jose-3.3.0/jose/jwt.py
450
451
452
verify_signature = defaults.get("verify_signature", True)
453
454
+ # Forbid the usage of the jwt.decode without alogrightms parameter
455
+ # See https://github.com/mpdavis/python-jose/issues/346 for more
456
+ # information CVE-2024-33663
457
+ if verify_signature and algorithms is None:
458
+ raise JWTError("It is required that you pass in a value for "
459
+ 'the "algorithms" argument when calling '
460
+ "decode().")
461
+
462
try:
463
payload = jws.verify(token, key, algorithms, verify=verify_signature)
464
except JWSError as e:
465
Index: python-jose-3.3.0/jose/backends/native.py
466
===================================================================
467
--- python-jose-3.3.0.orig/jose/backends/native.py
468
+++ python-jose-3.3.0/jose/backends/native.py
469
470
from jose.constants import ALGORITHMS
471
from jose.exceptions import JWKError
472
from jose.utils import base64url_decode, base64url_encode
473
+from jose.utils import is_pem_format, is_ssh_key
474
475
476
def get_random_bytes(num_bytes):
477
478
if isinstance(key, str):
479
key = key.encode("utf-8")
480
481
- invalid_strings = [
482
- b"-----BEGIN PUBLIC KEY-----",
483
- b"-----BEGIN RSA PUBLIC KEY-----",
484
- b"-----BEGIN CERTIFICATE-----",
485
- b"ssh-rsa",
486
- ]
487
-
488
- if any(string_value in key for string_value in invalid_strings):
489
+ if is_pem_format(key) or is_ssh_key(key):
490
raise JWKError(
491
"The specified key is an asymmetric key or x509 certificate and"
492
" should not be used as an HMAC secret."
493
Index: python-jose-3.3.0/jose/utils.py
494
===================================================================
495
--- python-jose-3.3.0.orig/jose/utils.py
496
+++ python-jose-3.3.0/jose/utils.py
497
498
+import re
499
import base64
500
import struct
501
502
503
if isinstance(s, str):
504
return s.encode("utf-8", "strict")
505
raise TypeError(f"not expecting type '{type(s)}'")
506
+
507
+
508
+# Based on https://github.com/jpadilla/pyjwt/commit/9c528670c455b8d948aff95ed50e22940d1ad3fc
509
+# Based on https://github.com/hynek/pem/blob/7ad94db26b0bc21d10953f5dbad3acfdfacf57aa/src/pem/_core.py#L224-L252
510
+_PEMS = {
511
+ b"CERTIFICATE",
512
+ b"TRUSTED CERTIFICATE",
513
+ b"PRIVATE KEY",
514
+ b"PUBLIC KEY",
515
+ b"ENCRYPTED PRIVATE KEY",
516
+ b"OPENSSH PRIVATE KEY",
517
+ b"DSA PRIVATE KEY",
518
+ b"RSA PRIVATE KEY",
519
+ b"RSA PUBLIC KEY",
520
+ b"EC PRIVATE KEY",
521
+ b"DH PARAMETERS",
522
+ b"NEW CERTIFICATE REQUEST",
523
+ b"CERTIFICATE REQUEST",
524
+ b"SSH2 PUBLIC KEY",
525
+ b"SSH2 ENCRYPTED PRIVATE KEY",
526
+ b"X509 CRL",
527
+}
528
+
529
+
530
+_PEM_RE = re.compile(
531
+ b"----[- ]BEGIN ("
532
+ + b"|".join(_PEMS)
533
+ + b""")[- ]----\r?
534
+.+?\r?
535
+----[- ]END \\1[- ]----\r?\n?""",
536
+ re.DOTALL,
537
+)
538
+
539
+
540
+def is_pem_format(key):
541
+ """
542
+ Return True if the key is PEM format
543
+ This function uses the list of valid PEM headers defined in
544
+ _PEMS dict.
545
+ """
546
+ return bool(_PEM_RE.search(key))
547
+
548
+
549
+# Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46
550
+_CERT_SUFFIX = b"-cert-v01@openssh.com"
551
+_SSH_PUBKEY_RC = re.compile(br"\A(\S+)[ \t]+(\S+)")
552
+_SSH_KEY_FORMATS = [
553
+ b"ssh-ed25519",
554
+ b"ssh-rsa",
555
+ b"ssh-dss",
556
+ b"ecdsa-sha2-nistp256",
557
+ b"ecdsa-sha2-nistp384",
558
+ b"ecdsa-sha2-nistp521",
559
+]
560
+
561
+
562
+def is_ssh_key(key):
563
+ """
564
+ Return True if the key is a SSH key
565
+ This function uses the list of valid SSH key format defined in
566
+ _SSH_KEY_FORMATS dict.
567
+ """
568
+ if any(string_value in key for string_value in _SSH_KEY_FORMATS):
569
+ return True
570
+
571
+ ssh_pubkey_match = _SSH_PUBKEY_RC.match(key)
572
+ if ssh_pubkey_match:
573
+ key_type = ssh_pubkey_match.group(1)
574
+ if _CERT_SUFFIX == key_type[-len(_CERT_SUFFIX) :]:
575
+ return True
576
+
577
+ return False
578
Index: python-jose-3.3.0/tests/algorithms/test_HMAC.py
579
===================================================================
580
--- python-jose-3.3.0.orig/tests/algorithms/test_HMAC.py
581
+++ python-jose-3.3.0/tests/algorithms/test_HMAC.py
582
583
584
def test_RSA_key(self):
585
key = "-----BEGIN PUBLIC KEY-----"
586
+ key += "\n\n\n-----END PUBLIC KEY-----"
587
with pytest.raises(JOSEError):
588
HMACKey(key, ALGORITHMS.HS256)
589
590
key = "-----BEGIN RSA PUBLIC KEY-----"
591
+ key += "\n\n\n-----END RSA PUBLIC KEY-----"
592
with pytest.raises(JOSEError):
593
HMACKey(key, ALGORITHMS.HS256)
594
595
key = "-----BEGIN CERTIFICATE-----"
596
+ key += "\n\n\n-----END CERTIFICATE-----"
597
with pytest.raises(JOSEError):
598
HMACKey(key, ALGORITHMS.HS256)
599
600