Skip to content

jwt: add opt-in strict_serialization to enforce compact form#381

Merged
simo5 merged 1 commit into
latchset:mainfrom
arpitjain099:feat/jwt-strict-compact-342
Jun 5, 2026
Merged

jwt: add opt-in strict_serialization to enforce compact form#381
simo5 merged 1 commit into
latchset:mainfrom
arpitjain099:feat/jwt-strict-compact-342

Conversation

@arpitjain099

Copy link
Copy Markdown
Contributor

Fixes #342

RFC 7519 (Section 7.1, step 8 and the definition of a JWT) requires that a JWT be represented using the JWS or JWE Compact Serialization. JWT.deserialize today decides what to do based on jwt.count('.'): two dots route to a JWS, four dots to a JWE. Because the JWS/JWE deserialize methods try json_decode first, a JSON-serialized token whose surrounding JSON happens to contain exactly two (JWS) or four (JWE) . characters is accepted as a JWT. That is reachable in practice: a dotted value in an unprotected header ({"kid":"a.b.c"}) or a per-recipient header lands the count on 2 or 4, so a JSON Serialization gets parsed where only a compact token should be.

As the issue notes this is not a vulnerability on its own, but parsing a representation the spec disallows is extra attack surface that some callers would rather not expose.

What this changes

A new opt-in strict_serialization keyword argument on JWT (default False). When it is True, deserialize rejects any JSON-serialized JWS/JWE with a clear ValueError and only accepts the compact form. Detection reuses the same json_decode check that JWS.deserialize / JWE.deserialize already use to tell the two representations apart (a complete compact token never decodes to a JSON object).

The default is False, so existing behavior is completely unchanged. This mirrors the existing knob style on this class (expected_type, the JWT_expect_type module flag) and has no effect on token creation, which already only emits the compact serialization.

Before / after

# A JSON-serialized JWS whose JSON contains exactly two dots
signer = jws.JWS(payload='{"sub":"alice"}')
signer.add_signature(key, alg='HS256', protected='{"alg":"HS256"}',
                     header={'kid': 'a.b.c'})
json_token = signer.serialize(compact=False)

# Default (unchanged): accepted
jwt.JWT().deserialize(json_token, key)            # ok

# Opt-in strict: rejected
jwt.JWT(strict_serialization=True).deserialize(json_token, key)
# ValueError: Only the JWS/JWE Compact Serialization is allowed for JWTs
#             (RFC 7519), but a JSON Serialization was provided

# Compact token still works in both modes
jwt.JWT(strict_serialization=True).deserialize(compact_token, key)  # ok

Tests

Added test_jwt_strict_serialization_jws and test_jwt_strict_serialization_jwe covering, for both signed and encrypted tokens:

  • flag on: JSON serialization rejected (via deserialize and via the constructor jwt= path), compact accepted
  • flag off (default): JSON serialization still accepted, so the change is backward compatible

Full suite is green (python -m pytest jwcrypto/tests.py), flake8 clean on the changed files.

I went with an opt-in flag deliberately to avoid changing the default. If you would rather have a differently named argument (for example serialization= taking "compact" / "any"), or want this surfaced as a module-level switch instead, happy to adjust.

RFC 7519 requires a JWT to use the JWS/JWE Compact Serialization, but
JWT.deserialize currently dispatches on the number of dots in the input
and will also accept a JSON Serialization whose surrounding JSON happens
to contain exactly two (JWS) or four (JWE) dot characters. A dotted value
in an unprotected or per-recipient header makes that reachable, so a JSON
Serialization can be parsed where only a compact token should be allowed.

Add an opt-in strict_serialization constructor flag (default False, so
existing behavior is unchanged). When enabled, deserialize rejects any
JSON-serialized token with a clear RFC 7519 error and only accepts the
compact representation, reducing the parser attack surface. Detection
reuses the same json_decode check that JWS/JWE deserialize already rely
on to tell the two representations apart.

Add regression tests covering JWS and JWE for both the flag-on (reject
JSON, accept compact) and the default flag-off (preserve current
behavior) paths.

Fixes #342

Signed-off-by: Arpit Jain <arpitjain099@gmail.com>

@simo5 simo5 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent proposal, I really like how you addressed it.

@simo5 simo5 merged commit db93855 into latchset:main Jun 5, 2026
17 checks passed
@simo5

simo5 commented Jun 5, 2026

Copy link
Copy Markdown
Member

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Jwt.JWT allows parsing tokens with json serialization

2 participants