Skip to content

fix: crash when decoding JWTs with null 'kid' header#1177

Open
teebee wants to merge 1 commit into
jpadilla:masterfrom
teebee:fix/null-kid-header
Open

fix: crash when decoding JWTs with null 'kid' header#1177
teebee wants to merge 1 commit into
jpadilla:masterfrom
teebee:fix/null-kid-header

Conversation

@teebee

@teebee teebee commented May 30, 2026

Copy link
Copy Markdown

Description

This PR fixes a regression introduced in commit 051ea34 (#GHSA-752w-5fwx-jx9f) where JWTs containing an explicit null value for the kid header parameter cause decode() to crash with an InvalidTokenError: Key ID header parameter must be a string.

According to RFC 7515, the kid parameter is optional. While it is usually omitted, some Identity Providers emit "kid": null when no specific key ID is assigned. Prior to the crit header validation update in v2.12.0, PyJWT safely ignored this during decoding.

Real-world Impact

This is not just a theoretical RFC edge-case. For example, the Confluent Kafka Metadata Server (MDS) issues production-level JWT tokens that explicitly include "kid": null in their header.

Due to the recent regression, any Python-based Kafka client or microservice trying to validate Confluent Kafka MDS tokens using PyJWT will instantly crash.

Changes

  • Updated _validate_headers in jwt/api_jws.py to only trigger _validate_kid if kid is present and not None.
  • Added a unit test to tests/test_api_jwt.py to ensure tokens with null keys are correctly tolerated during decoding without introducing regressions to other components.

@auvipy auvipy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tests are failing. please cross check

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to restore decoding compatibility for JWTs whose JOSE header explicitly contains "kid": null, while preserving header validation behavior elsewhere in PyJWT.

Changes:

  • Adjusts JWS header validation to skip kid type validation when the value is None.
  • Adds a JWT decode regression test for tokens with a null kid header.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
jwt/api_jws.py Updates kid validation logic in shared JWS header validation.
tests/test_api_jwt.py Adds coverage for decoding a JWT with an explicit null kid header.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread jwt/api_jws.py Outdated
Comment thread tests/test_api_jwt.py Outdated
Comment on lines +314 to +318
token = jwt.encode(
payload,
secret,
headers={"kid": None},
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I have refactored the test to use a pre-encoded, signed JWT string literal with a null 'kid' header (and 'iss': 'teebee' as payload).

@teebee teebee force-pushed the fix/null-kid-header branch from 7ea0462 to b94b58b Compare May 31, 2026 09:41
@teebee

teebee commented May 31, 2026

Copy link
Copy Markdown
Author

Hi @auvipy, thank you for the feedback!

I have refactored the test to use a pre-encoded, signed JWT string literal with a null 'kid' header (and 'iss': 'teebee' as payload).

This completely isolates the decoder behavior, removes the runtime dependency on jwt.encode() within the test case, and purely verifies that jwt.decode() executes without throwing an exception.

The fix and the refined test are now bundled into the amended commit.

Commit 051ea34 introduced _validate_headers during decoding, which triggers an InvalidTokenError if the 'kid' header property is explicitly set to null (None in Python). According to RFC 7515, 'kid' is optional, and while typically omitted, some Identity Providers emit 'kid': null by default. This change ensures _validate_kid is only executed if 'kid' is present and not None, restoring compatibility with Confluent MDS tokens, for instance.

Signed-off-by: teebee <733833@gmx.de>
@teebee teebee force-pushed the fix/null-kid-header branch from b94b58b to af5349b Compare May 31, 2026 10:34
@teebee

teebee commented May 31, 2026

Copy link
Copy Markdown
Author

Hi @auvipy, I have updated the branch.

I refined _validate_headers to only skip the null-value validation during decode (encoding=False). This ensures the strict test_encode_fails_on_invalid_kid_types contract remains fully preserved when creating new tokens.

@teebee

teebee commented May 31, 2026

Copy link
Copy Markdown
Author

Hi @auvipy,

Just a quick heads-up and my apologies: While integrating the suggestions from Copilot, my local git history got a bit tangled, and I accidentally overwrote your explicit commit (7ea0462). I honestly didn't notice your commit at the time, so please don't think I was trying to "improve" or discard your work on purpose!

However, I kept your exact logic intact. I just split it into a nested if block to add a clarifying comment about the null-valued kid behavior during decoding:

if "kid" in headers:
    # When decoding, treat a null-valued 'kid' header parameter
    # the same as if the parameter was omitted entirely.
    if encoding or headers["kid"] is not None:
        self._validate_kid(headers["kid"])

The logic remains 100% equivalent to your flat if "kid" in headers and (encoding or headers["kid"] is not None): approach.

If you prefer your original single-line statement for the sake of the history or clean hierarchy, please feel free to overwrite or amend this section directly. Sorry again for the mix-up!

@auvipy

auvipy commented May 31, 2026

Copy link
Copy Markdown
Collaborator

there is not need for force push

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.

3 participants