When jwt.decode() is called with an audience parameter, tokens that do not contain an aud claim are silently accepted instead of being rejected. This allows cross-service token reuse and potential privilege escalation.
Affected versions: All versions through 3.5.0 (latest)
Reproduction:
from jose import jwt
token = jwt.encode({"sub": "attacker", "role": "admin"}, "secret", algorithm="HS256")
This should raise but doesn't:
decoded = jwt.decode(token, "secret", algorithms=["HS256"], audience="my-service")
Returns: {'sub': 'attacker', 'role': 'admin'}
Expected behavior: When audience= is specified, tokens without an aud claim should be rejected (as PyJWT does with MissingRequiredClaimError).
Actual behavior: The token is accepted silently.
Impact: Any application using audience= without also setting options={"require_aud": True} is vulnerable to cross-service token reuse. An attacker with a valid token from one service (that doesn't set aud) can use it to access another service that expects a specific audience.
Comparison with PyJWT:
import jwt as pyjwt
token = pyjwt.encode({"sub": "attacker"}, "secret", algorithm="HS256")
pyjwt.decode(token, "secret", algorithms=["HS256"], audience="my-service")
Raises: MissingRequiredClaimError('aud')
Suggested fix:
In jose/jwt.py, _validate_aud():
def _validate_aud(claims, audience=None):
if "aud" not in claims:
if audience is not None:
raise JWTClaimsError("Token is missing the 'aud' claim")
return
# ... rest unchanged
Workaround: options={"require_aud": True} (non-obvious, not the default)
CWE: CWE-287 (Improper Authentication) / CWE-1188 (Insecure Default)
Discovered by: Frédéric Bogaerts, University of Coimbra (VAITP Research Project)
When jwt.decode() is called with an audience parameter, tokens that do not contain an aud claim are silently accepted instead of being rejected. This allows cross-service token reuse and potential privilege escalation.
Affected versions: All versions through 3.5.0 (latest)
Reproduction:
from jose import jwt
token = jwt.encode({"sub": "attacker", "role": "admin"}, "secret", algorithm="HS256")
This should raise but doesn't:
decoded = jwt.decode(token, "secret", algorithms=["HS256"], audience="my-service")
Returns: {'sub': 'attacker', 'role': 'admin'}
Expected behavior: When audience= is specified, tokens without an aud claim should be rejected (as PyJWT does with MissingRequiredClaimError).
Actual behavior: The token is accepted silently.
Impact: Any application using audience= without also setting options={"require_aud": True} is vulnerable to cross-service token reuse. An attacker with a valid token from one service (that doesn't set aud) can use it to access another service that expects a specific audience.
Comparison with PyJWT:
import jwt as pyjwt
token = pyjwt.encode({"sub": "attacker"}, "secret", algorithm="HS256")
pyjwt.decode(token, "secret", algorithms=["HS256"], audience="my-service")
Raises: MissingRequiredClaimError('aud')
Suggested fix:
In jose/jwt.py, _validate_aud():
def _validate_aud(claims, audience=None):
if "aud" not in claims:
if audience is not None:
raise JWTClaimsError("Token is missing the 'aud' claim")
return
# ... rest unchanged
Workaround: options={"require_aud": True} (non-obvious, not the default)
CWE: CWE-287 (Improper Authentication) / CWE-1188 (Insecure Default)
Discovered by: Frédéric Bogaerts, University of Coimbra (VAITP Research Project)