CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-103001

CVE-2026-103001: State Pollution in PyJWT Option Merging Leads to Claim Verification Bypass

Amit Schendel
Amit Schendel
Senior Security Researcher

Oct 1, 2026·6 min read·7 visits

Executive Summary (TL;DR)

A state pollution flaw in PyJWT's options merger mutates reused configuration dictionaries. When signature verification is toggled from False to True, critical claim verifications like expiration and audience checks remain permanently disabled, allowing expired or invalid tokens to be accepted.

PyJWT versions 2.11.0 through 2.13.0 suffer from a state pollution vulnerability in the `_merge_options` method. When an application passes a mutable configuration mapping with signature verification disabled, the library modifies the object in-place. If this same dictionary is reused for subsequent verified decode operations, standard claim verifications (such as expiration, audience, and issuer validation) are silently bypassed.

Vulnerability Overview

PyJWT is a Python implementation of JSON Web Token (JWT) standards widely used in web applications and backend microservices. Between versions 2.11.0 and 2.13.0, PyJWT is susceptible to a state pollution vulnerability within its internal option merging mechanism.

The vulnerability is triggered when a caller-supplied, mutable mapping (such as a standard Python dictionary) is passed to decode() or decode_complete() with signature verification set to disabled. Under these conditions, PyJWT's _merge_options() method directly modifies the caller's options mapping in-place, adding or modifying registered claim verification options to false.

This in-place modification permanently alters the configuration state of the reused mapping. If the calling application subsequently re-enables signature verification on that same object, the claim validation steps (such as expiration, audience, and issuer checks) remain disabled. This behavior allows attackers to present cryptographically valid but expired tokens, which the library accepts without throwing validation errors.

Root Cause Analysis

In Python, dictionary structures are passed by reference. When an application provides an options configuration dictionary to PyJWT.decode(), the library merges default options with user-provided settings inside jwt/api_jwt.py.

The logic in the vulnerable version of _merge_options() evaluates whether signature verification is disabled. If verify_signature is false, PyJWT defensively sets default verification flags to false directly within the input object itself rather than a localized copy. This operation mutates the caller's shared state directly.

When the application processes a second token and changes verify_signature back to true, the previously injected false parameters remain in the dictionary. Because PyJWT checks the specific dictionary keys for explicit verification overrides, the presence of these mutated keys bypasses checks for expiration (verify_exp), not-before (verify_nbf), issued-at (verify_iat), audience (verify_aud), issuer (verify_iss), subject (verify_sub), and JWT ID (verify_jti).

Code Analysis

The vulnerability lies in jwt/api_jwt.py inside the _merge_options function. Prior to the patch, the mapping passed to the function was not isolated from the original reference.

Below is the vulnerable implementation:

def _merge_options(self, options: Options | None = None) -> FullOptions:
    if options is None:
        return self.options
 
    # (defensive) set defaults for verify_x to False if verify_signature is False
    if not options.get("verify_signature", True):
        options["verify_exp"] = options.get("verify_exp", False)
        options["verify_nbf"] = options.get("verify_nbf", False)
        options["verify_iat"] = options.get("verify_iat", False)
        options["verify_aud"] = options.get("verify_aud", False)
        options["verify_iss"] = options.get("verify_iss", False)
        options["verify_sub"] = options.get("verify_sub", False)
        options["verify_jti"] = options.get("verify_jti", False)
    return {**self.options, **options}

The patch addresses this state pollution by converting the incoming mapping to a standard, shallow-copied dictionary utilizing dict(options) and casting it as an Options object. This ensures that any key assignment affects only the isolated copy of the options mapping.

Below is the patched implementation:

def _merge_options(self, options: Options | None = None) -> FullOptions:
    if options is None:
        return self.options
 
    # Convert and isolate the incoming options mapping to prevent pollution
    merged_options = cast("Options", dict(options))
 
    # (defensive) set defaults for verify_x to False if verify_signature is False
    if not merged_options.get("verify_signature", True):
        merged_options["verify_exp"] = merged_options.get("verify_exp", False)
        merged_options["verify_nbf"] = merged_options.get("verify_nbf", False)
        merged_options["verify_iat"] = merged_options.get("verify_iat", False)
        merged_options["verify_aud"] = merged_options.get("verify_aud", False)
        merged_options["verify_iss"] = merged_options.get("verify_iss", False)
        merged_options["verify_sub"] = merged_options.get("verify_sub", False)
        merged_options["verify_jti"] = merged_options.get("verify_jti", False)
    return {**self.options, **merged_options}

Exploitation & Proof of Concept

Exploitation of this vulnerability requires that the target application reuse an options configuration dictionary across multiple decode requests. An attacker targeting the application must provide an expired or claim-mismatched token that has been cryptographically signed by the correct backend secret.

The following Python script reproduces the state pollution and demonstrates the subsequent verification bypass:

import jwt
import time
 
SECRET_KEY = "super_secure_secret_key"
ALGORITHM = "HS256"
 
# Generate a token that expired in the past
expired_payload = {
    "user_id": 1337,
    "exp": int(time.time()) - 3600
}
expired_token = jwt.encode(payload=expired_payload, key=SECRET_KEY, algorithm=ALGORITHM)
 
# Create a reusable options mapping
shared_options = {
    "verify_signature": False
}
 
# Trigger state pollution via unverified decoding
jwt.decode(jwt=expired_token, options=shared_options)
 
# Attempt to enforce signature verification while reusing the polluted mapping
shared_options["verify_signature"] = True
 
try:
    # This call should raise ExpiredSignatureError, but it returns the decoded payload
    decoded = jwt.decode(
        jwt=expired_token,
        key=SECRET_KEY,
        algorithms=[ALGORITHM],
        options=shared_options
    )
    print("Vulnerable: Token accepted despite expiration check being active")
    print(decoded)
except jwt.ExpiredSignatureError:
    print("Secure: Expired signature exception raised")

If the application utilizes the vulnerable version of PyJWT and reuses shared_options, the script will bypass the expiration check. If the PyJWT installation has been upgraded or patched, an ExpiredSignatureError is raised.

Impact Assessment

The impact of this state pollution vulnerability is classified as Medium (CVSS 6.5). The complexity of the attack is High, as exploitation relies entirely on how the consuming application implements PyJWT and manages configuration lifecycles. If an application never reuses options mappings, the vulnerability cannot be triggered.

For systems that do reuse options configuration mappings, the vulnerability bypasses critical logical controls. Attackers can replay expired authentication tokens or present tokens intended for different audiences, which can result in privilege escalation or session hijacking.

The vulnerability is a regression of similar mutable dictionary issues identified in previous developer discussions (such as Issue 679). No active exploitation in the wild has been reported, and the vulnerability has not been added to CISA's Known Exploited Vulnerabilities catalog.

Remediation & Detection

To fully remediate CVE-2026-103001, update PyJWT to version 2.14.0 or higher. The fixed version ensures that all input mapping parameters are localized through copy operations prior to modifications.

In scenarios where immediate upgrading is not feasible, developers should adjust their implementation code to prevent dictionary reuse. Instead of modifying or referencing a shared dictionary, code should instantiate a fresh dictionary for each decode call:

# Insecure reuse pattern
jwt.decode(token, key, options=settings.JWT_CONFIG)
 
# Secure non-reused instantiation
jwt.decode(token, key, options=dict(settings.JWT_CONFIG))

Security teams can identify vulnerable patterns by scanning codebases with static analysis rules that alert on modified dictionary parameters passed directly to jwt.decode interfaces.

Fix Analysis (1)

Technical Appendix

CVSS Score
6.5/ 10
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:N

Affected Systems

Python applications utilizing PyJWT version 2.11.0 through 2.13.0 with reused or shared verification options dictionaries

Affected Versions Detail

Product
Affected Versions
Fixed Version
PyJWT
PyJWT Project
>= 2.11.0, <= 2.13.02.14.0
AttributeDetail
CWE IDCWE-471: Modification of Assumed-Immutable Data (MAID)
Attack VectorNetwork (Unauthenticated, high complexity due to application logic dependence)
CVSS v3.16.5 (Medium)
EPSS ScoreN/A
ImpactClaim verification bypass (Expiration, Audience, Issuer checks bypassed)
Exploit StatusProof-of-Concept (PoC) available
CISA KEV StatusNot Listed

MITRE ATT&CK Mapping

T1556Modify Authentication Process
Credential Access
CWE-471
Modification of Assumed-Immutable Data (MAID)

The product allows input to modify data that is assumed to be immutable, causing a state mutation which can be leveraged to bypass logical checks in subsequent program operations.

Known Exploits & Detection

GitHub Issue Tracker (Options Mutation Case)Original issue track outlining persistent side-effects on state dictionaries during merging procedures.

References & Sources

  • [1]GitHub Security Advisory: Claim Verification Bypass due to State Pollution
  • [2]Fix commit implementing dict conversion within _merge_options
  • [3]PyJWT Issue 679: Dictionary Mutated in-place During Decoding Process
  • [4]Official CVE-2026-103001 Record

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•43 minutes ago•CVE-2026-93981
4.7

CVE-2026-93981: Cross-Site Scripting via Unescaped SSR Pathways in Hono JSX Engine

An improper neutralization of input during web page generation (CWE-79) vulnerability exists in the server-side rendering JSX engine (hono/jsx) of the Hono web framework prior to version 4.13.7. The flaw enables unauthenticated remote attackers to execute arbitrary JavaScript in the victim's browser context by supplying unescaped HTML characters into user-controlled fields rendered within specific boundary components, context providers, or direct server-side utilities.

Alon Barad
Alon Barad
3 views•6 min read
•about 2 hours ago•GHSA-59CR-6R3X-644W
8.8

GHSA-59CR-6R3X-644W: GitPython Submodule Update Path Traversal Can Write Outside the Repository

A path traversal vulnerability exists in GitPython when handling submodule updates recursively. If an attacker crafts a malicious repository with traversed paths or symbolic links in the submodule configuration, they can execute arbitrary file writes outside the parent repository's working directory. This can lead to system configuration modifications or arbitrary code execution.

Amit Schendel
Amit Schendel
5 views•5 min read
•about 3 hours ago•GHSA-C2M8-H5V5-343R
7.5

GHSA-C2M8-H5V5-343R: Path Traversal via Improper Link Resolution in Tornado StaticFileHandler

A path traversal and arbitrary file disclosure vulnerability exists in Tornado's StaticFileHandler. In versions prior to 6.5.9, the handler follows symbolic links that point outside of the configured root static directory. This behavior occurs because the handler performs lexical path validation rather than physical filesystem resolution, allowing unauthenticated remote attackers to read arbitrary files if they can access or control symbolic links within the served static root.

Amit Schendel
Amit Schendel
5 views•4 min read
•about 4 hours ago•GHSA-CHX6-46F5-W4VP
7.5

GHSA-CHX6-46F5-W4VP: Uncontrolled Resource Consumption in Tornado CurlAsyncHTTPClient

A critical uncontrolled resource consumption vulnerability exists in the Tornado web server's libcurl-based HTTP client (CurlAsyncHTTPClient). When processing highly compressed responses with response decompression enabled, the client experiences unbounded memory growth. This leads to host memory exhaustion and denial of service via application crashes.

Amit Schendel
Amit Schendel
6 views•6 min read
•about 5 hours ago•GHSA-3HV7-MJH2-FV65
5.3

GHSA-3HV7-MJH2-FV65: Unbounded Query-String Parsing Denial of Service in Tornado Web Server

An uncontrolled resource consumption vulnerability in Tornado's HTTP query-string parser allows remote, unauthenticated attackers to trigger CPU exhaustion and block the single-threaded event loop via crafted request URIs containing large numbers of parameters.

Amit Schendel
Amit Schendel
5 views•6 min read
•about 7 hours ago•CVE-2026-92081
5.9

CVE-2026-92081: Denial of Service via Uncaught Exception on HTTP/2 Response Trailers in Fastify

A protocol validation vulnerability exists in Fastify before version 5.12.5. When serving requests over HTTP/2, Fastify unconditionally injects the forbidden Transfer-Encoding header when response trailers are used, triggering an uncaught exception in Node.js and crashing the process.

Alon Barad
Alon Barad
4 views•7 min read