CVE-2022-39227 (HTML, published 2022-09-23T07:15Z)
Description: value: python-jwt is a module for generating and verifying JSON Web Tokens. Versions prior to 3.3.4 are subject to Authentication Bypass by Spoofing, resulting in identity spoofing, session hijacking or authentication bypass. An attacker who obtains a JWT can arbitrarily forge its contents without knowing the secret key. Depending on the application, this may for example enable the attacker to spoof other user's identities, hijack their sessions, or bypass authentication. Users should upgrade to version 3.3.4. There are no known workarounds.
Severity: nan (CVSS v3 base score: 9.1)
Weakness: CWE-290 \u2014 Authentication Bypass by Spoofing
This attack-focused weakness is caused by incorrectly implemented authentication schemes that are subject to spoofing attacks.
Repository: https://github.com/davedoesdev/python-jwt
Files changed:
README.md
coverage/coverage.xml
coverage/html/index.html
coverage/html/status.json
docs/_build/html/.buildinfo
docs/_build/html/_static/documentation_options.js
docs/_build/html/genindex.html
docs/_build/html/index.html
docs/_build/html/py-modindex.html
docs/_build/html/search.html
docs/conf.py
python_jwt/__init__.py
setup.py
test/vulnerability_vows.py
Commit message: FIX VULNERABILITY

jwcrypto accepts both compact and JSON formats.
It was possible to use this to present a token with arbitrary
claims with a signature from another valid token.

See test/vulnerability_vows.py for an example.


Vulnerable code:
<coverage version="6.4.1" timestamp="1656832390379" lines-valid="88" lines-covered="88" line-rate="1" branches-valid="58" branches-covered="58" branch-rate="1" complexity="0">
						<line number="12" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="15" hits="1"/>
						<line number="17" hits="1"/>
						<line number="59" hits="1"/>
						<line number="64" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="65" hits="1"/>
						<line number="66" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="67" hits="1"/>
						<line number="70" hits="1"/>
						<line number="72" hits="1"/>
						<line number="74" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="75" hits="1"/>
						<line number="77" hits="1"/>
						<line number="80" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="81" hits="1"/>
						<line number="82" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="83" hits="1"/>
						<line number="85" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="86" hits="1"/>
						<line number="88" hits="1"/>
						<line number="93" hits="1"/>
						<line number="101" hits="1"/>
						<line number="142" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="143" hits="1"/>
						<line number="145" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="147" hits="1"/>
						<line number="149" hits="1"/>
						<line number="151" hits="1"/>
						<line number="154" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="155" hits="1"/>
						<line number="156" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="159" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="160" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="161" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="162" hits="1"/>
						<line number="163" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="164" hits="1"/>
						<line number="168" hits="1"/>
						<line number="169" hits="1"/>
						<line number="171" hits="1"/>
						<line number="173" hits="1"/>
						<line number="175" hits="1"/>
						<line number="176" hits="1"/>
						<line number="179" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="180" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="181" hits="1"/>
						<line number="182" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="183" hits="1"/>
						<line number="185" hits="1"/>
						<line number="186" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="187" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="188" hits="1"/>
						<line number="189" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="190" hits="1"/>
						<line number="194" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="195" hits="1"/>
						<line number="196" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="197" hits="1"/>
						<line number="201" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="202" hits="1"/>
						<line number="203" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="204" hits="1"/>
						<line number="222" hits="1"/>
						<line number="223" hits="1"/>
						<line number="224" hits="1"/>
						<line number="225" hits="1"/>
            created at 2022-07-03 08:13 +0100
                <td>88</td>
                <td>58</td>
                <td class="right" data-ratio="146 146">100%</td>
                <td>88</td>
                <td>58</td>
                <td class="right" data-ratio="146 146">100%</td>
            created at 2022-07-03 08:13 +0100
{"format":2,"version":"6.4.1","globals":"808b5cd956a194fa2027f5f6eb2aaf06","files":{"d_7bfe6de028c7ad70___init___py":{"hash":"eee4edc908a3614db375ba99356b09c0","index":{"nums":[0,1,88,0,0,58,0,0],"html_filename":"d_7bfe6de028c7ad70___init___py.html","relative_filename":"python_jwt/__init__.py"}}}}
config: e1ca4cbd0b61431a5536cbdf2d794e81
    VERSION: '3.3.3',
    <title>Index &#8212; python-jwt 3.3.3 documentation</title>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.3 documentation</a> &#187;</li>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.3 documentation</a> &#187;</li>
    <title>python_jwt module &#8212; python-jwt 3.3.3 documentation</title>
        <li class="nav-item nav-item-0"><a href="#">python-jwt 3.3.3 documentation</a> &#187;</li>
        <li class="nav-item nav-item-0"><a href="#">python-jwt 3.3.3 documentation</a> &#187;</li>
    <title>Python Module Index &#8212; python-jwt 3.3.3 documentation</title>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.3 documentation</a> &#187;</li>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.3 documentation</a> &#187;</li>
    <title>Search &#8212; python-jwt 3.3.3 documentation</title>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.3 documentation</a> &#187;</li>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.3 documentation</a> &#187;</li>
release = '3.3.3'

    parsed_claims = json_decode(base64url_decode(claims))
    version='3.3.3',

Fixed code:
- **Note:** Versions 3.3.4 and later fix a vulnerability in JSON Web Token verification which lets an attacker with a valid token to re-use its signature with modified claims. CVE to follow. Please upgrade!
<coverage version="6.4.1" timestamp="1661973013420" lines-valid="96" lines-covered="96" line-rate="1" branches-valid="60" branches-covered="60" branch-rate="1" complexity="0">
						<line number="10" hits="1"/>
						<line number="13" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="16" hits="1"/>
						<line number="18" hits="1"/>
						<line number="60" hits="1"/>
						<line number="65" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="66" hits="1"/>
						<line number="67" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="69" hits="1"/>
						<line number="71" hits="1"/>
						<line number="73" hits="1"/>
						<line number="75" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="76" hits="1"/>
						<line number="79" hits="1"/>
						<line number="81" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="82" hits="1"/>
						<line number="83" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="84" hits="1"/>
						<line number="86" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="87" hits="1"/>
						<line number="92" hits="1"/>
						<line number="94" hits="1"/>
						<line number="102" hits="1"/>
						<line number="103" hits="1"/>
						<line number="104" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="105" hits="1"/>
						<line number="107" hits="1"/>
						<line number="150" hits="1"/>
						<line number="152" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="155" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="159" hits="1"/>
						<line number="161" hits="1"/>
						<line number="163" hits="1"/>
						<line number="164" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="165" hits="1"/>
						<line number="169" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="171" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="172" hits="1"/>
						<line number="173" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="174" hits="1"/>
						<line number="176" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="177" hits="1"/>
						<line number="179" hits="1"/>
						<line number="180" hits="1"/>
						<line number="181" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="182" hits="1"/>
						<line number="184" hits="1"/>
						<line number="186" hits="1"/>
						<line number="187" hits="1"/>
						<line number="189" hits="1"/>
						<line number="190" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="191" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="194" hits="1"/>
						<line number="196" hits="1"/>
						<line number="197" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="198" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="201" hits="1"/>
						<line number="203" hits="1"/>
						<line number="204" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="205" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="207" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="208" hits="1"/>
						<line number="211" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="212" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="213" hits="1"/>
						<line number="214" hits="1" branch="true" condition-coverage="100% (2/2)"/>
						<line number="215" hits="1"/>
						<line number="217" hits="1"/>
						<line number="221" hits="1"/>
						<line number="233" hits="1"/>
						<line number="234" hits="1"/>
						<line number="235" hits="1"/>
						<line number="236" hits="1"/>
						<line number="237" hits="1"/>
            created at 2022-08-31 20:10 +0100
                <td>96</td>
                <td>60</td>
                <td class="right" data-ratio="156 156">100%</td>
                <td>96</td>
                <td>60</td>
                <td class="right" data-ratio="156 156">100%</td>
            created at 2022-08-31 20:10 +0100
{"format":2,"version":"6.4.1","globals":"808b5cd956a194fa2027f5f6eb2aaf06","files":{"d_7bfe6de028c7ad70___init___py":{"hash":"a3460828b320d5b58e5d56aeda5982f0","index":{"nums":[0,1,96,0,0,60,0,0],"html_filename":"d_7bfe6de028c7ad70___init___py.html","relative_filename":"python_jwt/__init__.py"}}}}
config: 0bdc22a8bc7fdc3e0b3b52be398c1405
    VERSION: '3.3.4',
    <title>Index &#8212; python-jwt 3.3.4 documentation</title>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.4 documentation</a> &#187;</li>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.4 documentation</a> &#187;</li>
    <title>python_jwt module &#8212; python-jwt 3.3.4 documentation</title>
        <li class="nav-item nav-item-0"><a href="#">python-jwt 3.3.4 documentation</a> &#187;</li>
        <li class="nav-item nav-item-0"><a href="#">python-jwt 3.3.4 documentation</a> &#187;</li>
    <title>Python Module Index &#8212; python-jwt 3.3.4 documentation</title>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.4 documentation</a> &#187;</li>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.4 documentation</a> &#187;</li>
    <title>Search &#8212; python-jwt 3.3.4 documentation</title>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.4 documentation</a> &#187;</li>
        <li class="nav-item nav-item-0"><a href="index.html">python-jwt 3.3.4 documentation</a> &#187;</li>
release = '3.3.4'
import re
_jwt_re = re.compile(r'^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*$')
def _check_jwt_format(jwt):
    if not _jwt_re.match(jwt):
        raise _JWTError('invalid JWT format')

    #pylint: disable=too-many-statements

    _check_jwt_format(jwt)

        parsed_claims = json_decode(token.payload)
    else:
        parsed_claims = json_decode(base64url_decode(claims))
    _check_jwt_format(jwt)
    version='3.3.4',
""" Test claim forgery vulnerability fix """
from datetime import timedelta
from json import loads, dumps
from test.common import generated_keys
from test import python_jwt as jwt
from pyvows import Vows, expect
from jwcrypto.common import base64url_decode, base64url_encode

@Vows.batch
class ForgedClaims(Vows.Context):
    """ Check we get an error when payload is forged using mix of compact and JSON formats """
    def topic(self):
        """ Generate token """
        payload = {'sub': 'alice'}
        return jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60))

    class PolyglotToken(Vows.Context):
        """ Make a forged token """
        def topic(self, topic):
            """ Use mix of JSON and compact format to insert forged claims including long expiration """
            [header, payload, signature] = topic.split('.')
            parsed_payload = loads(base64url_decode(payload))
            parsed_payload['sub'] = 'bob'
            parsed_payload['exp'] = 2000000000
            fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
            return '{"  ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'

        class Verify(Vows.Context):
            """ Check the forged token fails to verify """
            @Vows.capture_error
            def topic(self, topic):
                """ Verify the forged token """
                return jwt.verify_jwt(topic, generated_keys['PS256'], ['PS256'])

            def token_should_not_verify(self, r):
                """ Check the token doesn't verify due to mixed format being detected """
                expect(r).to_be_an_error()
                expect(str(r)).to_equal('invalid JWT format')