CVE-2026-48525: PyJWT: Unauthenticated DoS via unbounded Base64URL decoding of unused payload segment in b64=false detached JWS

Published May 28, 2026
·
Updated

> [!NOTE] > Practical impact depends on whether request body-size limits are enforced upstream (proxy/web-server/framework). Deployments with typical body-size caps (≤2 MB) bound the amplifier significantly; deployments accepting larger token inputs are more exposed.

When verifying detached JWS tokens using the unencoded-payload option ("b64": false, RFC 7797), PyJWT performs Base64URL decoding of the compact-serialization payload segment before enforcing the detached-payload rules.

For b64=false, PyJWT later discards that decoded payload and replaces it with the caller-provided detachedpayload. In practice, this turns the middle segment into an attacker-controlled “work amplifier”: a remote client can supply an arbitrarily large Base64URL payload segment that forces CPU work + memory allocations even if the signature is invalid.

This creates an unauthenticated DoS vector against any endpoint that verifies detached JWS using PyJWT.

---

Affected Component(s)

jwt/apijws.py

PyJWS.decode() / PyJWS.decodecomplete() load() (parsing and Base64URL decoding)

---

Root Cause (exact logic flaw)

What happens in the code

In jwt/apijws.py, decodecomplete() does the following (order matters):

Calls load(jwt) first, which decodes the token segments Only after that, checks header.get("b64") and if False, it replaces payload = detachedpayload and rebuilds the signing input

This behavior is visible in decodecomplete():

load(jwt) happens before the b64=false handling then payload = detachedpayload and signinginput = ... detachedpayload happens afterward ([GitHub][1])

Inside load(), PyJWT unconditionally performs:

payload = base64urldecode(payloadsegment) This is the expensive step the attacker can amplify ([GitHub][1])

Why this becomes a vulnerability

For b64=false detached JWS, the payload segment in compact form is effectively not needed for verification in PyJWT’s own logic (since the library uses detachedpayload as the real payload). Yet PyJWT still decodes it first, meaning:

cost is paid even when signature is invalid the decoded bytes are discarded attacker controls the size of this cost via token length

---

Impact (evidence-driven)

Security impact

Unauthenticated remote DoS: decoding work happens before signature rejection → attacker does not need signing key. CPU amplification: Base64URL decode time scales linearly with payload segment size. Memory amplification: decoded output allocates large byte buffers (tens of MB per request). Operational impact: request queueing / worker starvation under modest concurrency bursts.

Standards context (RFC 7797)

RFC 7797 explicitly notes this option is used when payload is large and/or detached, and discusses interoperability requirements around marking it critical (“crit” with “b64”). ([IETF Datatracker][2]) (PyJWT supports crit validation, but the issue here is decode order / unbounded decode of an unused segment.)

---

Affected Versions

Confirmed affected: PyJWT 2.12.1 (tested from your local editable install and repo). Likely affected: all versions that include detached payload support for JWS decoding, which was introduced in 2.4.0 (“Add detached payload support for JWS encoding and decoding”). ([pyjwt.readthedocs.io][3])

(For GHSA, this phrasing is strong: “confirmed” + “likely since feature introduction”.)

---

Threat Model

Typical real deployment

A service verifies signed HTTP requests or webhooks using detached JWS:

token is provided in JSON body / query / header actual payload is the HTTP request body passed as detachedpayload

Attacker

remote unauthenticated client can send requests to verify endpoint does not need a valid signature (invalid signature still triggers the expensive decode path)

Attack chain

1. Attacker crafts a JWS compact token with header containing "b64": false and crit:["b64"]. 2. Attacker inflates the payload segment (middle segment) to millions of Base64URL characters. 3. Server calls PyJWS.decode(...detachedpayload=...). 4. PyJWT decodes the inflated segment (CPU + memory). 5. Signature is rejected afterward (401) — but resources already consumed. 6. Repeated requests or bursts cause queueing/worker starvation → DoS.

---

Proof of Concept - file names + results

PoC placement

serverlocalhost.py

clientlocalhost.py

floodlocalhost.py

---

PoC # 1 - Localhost verification server

File: serverlocalhost.py

Purpose: real HTTP endpoint (POST /verify) that calls PyJWT detached verification and prints: ok / timems / peakbytes / tokenlen / error.

Results (server console output)

text [+] Listening on http://127.0.0.1:8000 [+] POST /verify JSON: {"token": "..."}

[127.0.0.1] ok=True timems=0.102 peakbytes=2624 tokenlen=117 err=None [127.0.0.1] ok=False timems=2.012 peakbytes=2000983 tokenlen=500078 err=InvalidSignatureError [127.0.0.1] ok=True timems=1.591 peakbytes=2001061 tokenlen=500117 err=None

[127.0.0.1] ok=True timems=0.065 peakbytes=2304 tokenlen=117 err=None [127.0.0.1] ok=False timems=7.534 peakbytes=8000983 tokenlen=2000078 err=InvalidSignatureError [127.0.0.1] ok=True timems=6.347 peakbytes=8001061 tokenlen=2000117 err=None

[127.0.0.1] ok=True timems=0.066 peakbytes=2304 tokenlen=117 err=None [127.0.0.1] ok=False timems=23.034 peakbytes=32000983 tokenlen=8000078 err=InvalidSignatureError [127.0.0.1] ok=True timems=22.097 peakbytes=32001061 tokenlen=8000117 err=None

Key takeaways from these results

At 8,000,000 chars, a single invalid-signature request still causes:

~23 ms server work ~32 MB peak allocations returns 401 (invalid signature) → attacker does not need key.

---

PoC # 2 - Localhost network client

File: clientlocalhost.py Purpose: generates baseline + (invalid signature) + (valid signature) tokens and sends them over HTTP to localhost server.

Results (client output)

payload-chars = 500,000

text === BASELINE (valid b64=false token) === HTTP: 200 clientwallms: 6.3499... servertimems: 0.10197... serverpeakbytes: 2624

=== ATTACK (INVALID signature - attacker needs no key) === HTTP: 401 clientwallms: 4.1010... servertimems: 2.01217... serverpeakbytes: 2000983 error: InvalidSignatureError

=== ATTACK (VALID signature - accepted path still wastes) === HTTP: 200 clientwallms: 3.6586... servertimems: 1.59092... serverpeakbytes: 2001061

payload-chars = 2,000,000

text === BASELINE === HTTP: 200 servertimems: 0.06527... serverpeakbytes: 2304

=== ATTACK (INVALID signature) === HTTP: 401 servertimems: 7.53430... serverpeakbytes: 8000983

=== ATTACK (VALID signature) === HTTP: 200 servertimems: 6.34682... serverpeakbytes: 8001061

payload-chars = 8,000,000

text === BASELINE === HTTP: 200 servertimems: 0.06573... serverpeakbytes: 2304

=== ATTACK (INVALID signature) === HTTP: 401 servertimems: 23.03403... serverpeakbytes: 32000983

=== ATTACK (VALID signature) === HTTP: 200 servertimems: 22.09702... serverpeakbytes: 32001061

Why this is strong evidence

The server clearly does heavy work before rejecting invalid signatures. The “valid signature” case shows even accepted requests waste resources due to unused payload segment.

---

PoC # 3 - Localhost flood / burst concurrency

File: floodlocalhost.py Purpose: sends N concurrent invalid-signature requests over HTTP to demonstrate queueing/worker starvation.

Results (your run: 20 concurrent @ 8,000,000 chars)

text totalwallms: 1374.5405770000616

(16, 401, 1156.4504789998864, 21.350951999920653, 32000983, 'InvalidSignatureError') (19, 401, 1151.2852699997893, 21.208721999755653, 32000983, 'InvalidSignatureError') (18, 401, 1102.7211239997996, 21.685218999664357, 32000983, 'InvalidSignatureError') (13, 401, 1102.0718189997751, 21.26572200040755, 32000983, 'InvalidSignatureError') (11, 401, 1095.9345460000804, 20.586017000368884, 32000983, 'InvalidSignatureError') (17, 401, 1085.2552810001725, 22.893039000337012, 32000983, 'InvalidSignatureError') (10, 401, 1078.3629560000918, 22.737160999895423, 32000983, 'InvalidSignatureError') (7, 401, 1048.2011740000416, 22.476282000297942, 32000983, 'InvalidSignatureError') (8, 401, 378.93017700025666, 21.377330999712285, 32000983, 'InvalidSignatureError') (1, 401, 281.45106800002395, 21.34223099983501, 32000983, 'InvalidSignatureError')

Interpretation

Each request still costs ~20–23 ms server processing and ~32 MB peak allocations. But client-observed latency rises up to ~1.15 seconds because requests queue behind each other → clear worker starvation/HoL blocking. All were rejected with 401 InvalidSignatureError → still unauthenticated.

---

Fix

Goal

Prevent unbounded resource consumption from an attacker-controlled payload segment that is unused in b64=false detached flow.

Minimal change strategy

In load() (or by refactoring parse order), do not Base64-decode payloadsegment until after you know whether b64=false applies.

Two safe options:

1. Reject non-empty payload segment when b64=false

Parse header first If b64 is false and payloadsegment is non-empty → raise DecodeError before decoding Then verification uses detachedpayload only

2. Skip decoding payload segment entirely when b64=false

Keep payload segment as raw bytes or empty Use detached payload for signing input

This aligns with the idea that detached payload is the trusted payload input for verification; the compact payload segment should not become a resource amplification vector.

(Implementation context: the current decode order and unconditional base64urldecode(payloadsegment) are visible in the file and line region around load() and decodecomplete() ([GitHub][1]).)

---

Workarounds

Enforce strict max token length at the HTTP boundary (proxy/gateway). Apply rate limiting on verification endpoints. If detached JWS (b64=false) is not needed in your app, reject tokens where header includes "b64": false.

Other sources

PyJWT is a JSON Web Token implementation in Python. From 2.8.0 to 2.12.1, when verifying detached JWS tokens using the unencoded-payload option ("b64": false, RFC 7797), PyJWT performs Base64URL decoding of the compact-serialization payload segment before enforcing the detached-payload rules. For b64=false, PyJWT later discards that decoded payload and replaces it with the caller-provided detachedpayload. In practice, this turns the middle segment into an attacker-controlled “work amplifier”: a remote client can supply an arbitrarily large Base64URL payload segment that forces CPU work + memory allocations even if the signature is invalid. This creates an unauthenticated DoS vector against any endpoint that verifies detached JWS using PyJWT. This vulnerability is fixed in 2.13.0.

MITRE

Affected Software

6 affected componentsFixes available
pypi/pyjwt>=2.8.0<=2.12.1
pip/pyjwt>=2.8.0<=2.12.1
2.13.0
Pyjwt Project Pyjwt>=2.8.0<=2.12.1
IBM Engineering AI Hub<=1.0.0
IBM Engineering AI Hub<=1.1.0
IBM Engineering AI Hub<=1.2.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/pyjwt to a version that resolves this vulnerability.

    Fixed in 2.13.0
  2. Upgrade

    Upgrade PyJWT to a version that resolves this vulnerability.

    Fixed in 2.13.0
  3. Configuration

    If your application does not require RFC 7797 unencoded-payload support, reject any JWS tokens whose protected header contains "b64": false (so PyJWT’s detached JWS path is not exercised).

    PyJWT JWS verification (RFC 7797 detached payload / "b64": false) Reject tokens where header includes "b64": false = true
  4. Compensating control

    Enforce strict max token length at the HTTP boundary (e.g., proxy/gateway body size / request size limits) to bound the attacker-controlled Base64URL payload segment size used for CPU/memory amplification.

Event History

May 28, 2026
CVE Published
via MITRE·03:11 PM
Data Sourced
via MITRE·03:11 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·04:16 PM
DescriptionSeverityWeaknessAffected Software
Jun 15, 2026
Advisory Published
via GitHub·07:29 PM
Data Sourced
via GitHub·07:29 PM
DescriptionSeverityWeaknessAffected Software
Jul 14, 2026
Data Sourced
via IBM·12:00 AM
DescriptionAffected Software

Parent advisories

This vulnerability appears in the following advisories.

Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-48525?

The severity of CVE-2026-48525 is medium with a score of 5.3.

2

How do I fix CVE-2026-48525?

To fix CVE-2026-48525, upgrade PyJWT to version 2.12.2 or later.

3

What is the impact of CVE-2026-48525?

CVE-2026-48525 can lead to an unauthenticated Denial of Service (DoS) attack.

4

Which versions of PyJWT are affected by CVE-2026-48525?

CVE-2026-48525 affects PyJWT versions from 2.8.0 to 2.12.1.

5

What is the nature of the vulnerability in CVE-2026-48525?

CVE-2026-48525 involves unbounded Base64URL decoding when verifying detached JWS tokens.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203