Skip to content

fix: reject tokens missing 'aud' claim when audience is expected#412

Open
gaoflow wants to merge 1 commit into
mpdavis:masterfrom
gaoflow:fix-missing-aud-claim-acceptance
Open

fix: reject tokens missing 'aud' claim when audience is expected#412
gaoflow wants to merge 1 commit into
mpdavis:masterfrom
gaoflow:fix-missing-aud-claim-acceptance

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 25, 2026

Copy link
Copy Markdown

Problem

jwt.decode(token, key, audience="my-service") silently accepts tokens that carry no aud claim at all. An attacker who holds a valid token from any service that omits aud can replay it against a service that checks audience, because the missing-claim path simply returns without raising.

Minimal reproduction:

from jose import jwt

token = jwt.encode({"sub": "attacker"}, "secret", algorithm="HS256")
# No 'aud' claim in the token

decoded = jwt.decode(token, "secret", algorithms=["HS256"], audience="my-service")
# Returns the claims instead of raising — cross-service replay works

PyJWT raises MissingRequiredClaimError('aud') in the same situation.

The root cause is a commented-out guard in _validate_aud (jwt.py):

if "aud" not in claims:
    # if audience:
    #     raise JWTError('Audience claim expected, but not in claims')
    return   # ← always returns, even when audience= was given

Fix

Activate the guard: raise JWTClaimsError when the caller provides a non-None audience but the token has no aud claim.

The existing options={"require_aud": True} opt-in continues to work as before for callers who need to require the claim without specifying an expected value.

Tests

  • test_aud_empty_claim (which documented the wrong behavior) is replaced by two explicit cases:
    • test_aud_missing_claim_no_audience — no audience expected → still passes
    • test_aud_missing_claim_with_audience — audience given but claim missing → raises JWTError
  • test_require is corrected to not pass an unrelated audience= value when testing require_<other-claim>.

All 250 tests pass (6 skipped, unchanged).

Closes #407


This pull request was prepared with the assistance of AI, under my direction and review.

When jwt.decode() is called with an audience= argument, tokens that do
not carry an 'aud' claim were silently accepted. This enables cross-
service token reuse: a valid token issued without an audience claim
could be replayed against any service that specifies an expected
audience.

The fix activates the commented-out guard in _validate_aud: if the
caller provides a non-None audience and the token has no 'aud' claim,
raise JWTClaimsError instead of returning. The existing require_aud=True
option remains the way to enforce the claim when audience= is not given.

Tests: rename test_aud_empty_claim to two explicit cases (no-audience
passes, audience-given raises), and fix test_require to not pass an
audience value for non-aud required-claim tests.

Fixes mpdavis#407
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.

[Security] jwt.decode() with audience= accepts tokens missing 'aud' claim (CWE-287)

1 participant