Skip to content

security: add optional key parameter to JWToken for signature verification#4085

Closed
dfgvaetyj3456356-hash wants to merge 7 commits into
redis:masterfrom
dfgvaetyj3456356-hash:security/fix-jwt-signature-verification
Closed

security: add optional key parameter to JWToken for signature verification#4085
dfgvaetyj3456356-hash wants to merge 7 commits into
redis:masterfrom
dfgvaetyj3456356-hash:security/fix-jwt-signature-verification

Conversation

@dfgvaetyj3456356-hash

@dfgvaetyj3456356-hash dfgvaetyj3456356-hash commented May 28, 2026

Copy link
Copy Markdown

This PR fixes a JWT signature verification bypass in JWToken.

CWE: CWE-345 (Insufficient Verification of Cryptographic Signature)
File: redis/auth/token.py

JWToken.init() previously decoded JWTs with verify_signature=False, meaning any forged token would be accepted regardless of the signing key. An attacker could craft arbitrary JWT payloads and have them accepted by downstream authorization decisions that rely on is_expired(), ttl(), and try_get().

Fix:

  • Added an optional key parameter to JWToken.init()
  • When key is provided, the JWT signature is verified using standard PyJWT verification
  • When key is None, existing behavior is preserved for backward compatibility
  • Updated existing tests to pass the signing key
  • Added new test verifying forged tokens are rejected with wrong key

Note

Medium Risk
Fixes a real signature-bypass (CWE-345) in auth code, but callers who still use JWToken(token) alone remain unverified until they pass key and algorithms.

Overview
JWToken now accepts optional key and algorithms. When key is set, PyJWT verifies the signature (and requires algorithms); aud / nbf checks stay off. With no key, behavior is unchanged: decode without signature verification for backward compatibility.

Tests and mocks were updated so the default mock identity provider uses verified tokens; a legacy mock path and dedicated signature tests cover the old init and forged-token rejection.

Reviewed by Cursor Bugbot for commit 7ee2ff9. Bugbot is set up for automated code reviews on this repo. Configure here.

@jit-ci

jit-ci Bot commented May 28, 2026

Copy link
Copy Markdown

Hi, I’m Jit, a friendly security platform designed to help developers build secure applications from day zero with an MVS (Minimal viable security) mindset.

In case there are security findings, they will be communicated to you as a comment inside the PR.

Hope you’ll enjoy using Jit.

Questions? Comments? Want to learn more? Get in touch with us.

Comment thread redis/auth/token.py Outdated
Comment thread redis/auth/token.py
@dfgvaetyj3456356-hash dfgvaetyj3456356-hash force-pushed the security/fix-jwt-signature-verification branch from 49b1761 to 6f0f6e8 Compare May 28, 2026 11:53
Comment thread redis/auth/token.py
…ignature verification

Signed-off-by: Security Researcher <security@example.com>
@dfgvaetyj3456356-hash dfgvaetyj3456356-hash force-pushed the security/fix-jwt-signature-verification branch from 6f0f6e8 to 3d51ef7 Compare May 28, 2026 12:22
Comment thread redis/auth/token.py
…rovided

When a key is provided, the code only disabled verify_exp but left
verify_aud and verify_nbf at their PyJWT defaults of True. This caused
DecodeError for any JWT containing an aud claim because no audience
parameter was passed to jwt.decode().

Fix by adding verify_aud=False and verify_nbf=False to the options
when key is provided, matching the behavior of the no-key path which
used verify_signature=False to implicitly disable all claim verifications.

Also add tests for JWT tokens with audience and nbf claims.

Signed-off-by: Security Researcher <security@example.com>

@petyaslavova petyaslavova 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.

Hey @dfgvaetyj3456356-hash, thank you for your contribution!
Please take a look at the requested changes and additionally please fix linters and ensure all code paths are covered with tests.
With the current change, the old, still supported, code path is completely skipped in the tests.
We should have coverage for the old initialization, when only token is provided, as well as when we have just algorithms and key is None, and when both key and algorithm are provided.
Also the case when a key is provided and algorithms is None - then we should validate that an error is raised.

Comment thread redis/auth/token.py Outdated
REQUIRED_FIELDS = {"exp"}

def __init__(self, token: str):
def __init__(self, token: str, key: str = None, algorithms: list = None):

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.

Please follow PEP 604 for the type hints

Comment thread redis/auth/token.py Outdated
)
if key is not None:
if algorithms is None:
algorithms = ["HS256"]

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.

When key is provided but algorithms is omitted, the code silently uses ["HS256"].
That default is risky and likely wrong for real IdP tokens; for example, Entra ID / Azure AD commonly uses RS256.
So in this case, it will be better to require a value for algorithms arg when key is provided.
A test for key provided without algorithms would make this behavior explicit and prevent the silent HS256 fallback from coming back later.
Please also add a docstring describing the behaviour and the arguments expectations.
Add a clear statement that this change doesn't completely resolve the security issue - using key and algorithms enables a safer path, but the old unsafe usage pattern is still available for compatibility.

Comment thread redis/auth/token.py Outdated
self._value,
key,
algorithms=algorithms,
options={"verify_exp": False, "verify_aud": False, "verify_nbf": False},

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.

Please don’t add verify_exp=False here. This disables expiration validation, so expired tokens can still decode successfully.
Expiration validation should remain enforced by default; otherwise, we introduce a new security problem by allowing expired tokens to be accepted.

Comment thread redis/auth/token.py Outdated
algorithms = [header["alg"]]
self._decoded = jwt.decode(
self._value,
options={"verify_signature": False, "verify_exp": False},

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.

ditto

Comment thread tests/conftest.py
token = {"exp": datetime.now(timezone.utc).timestamp() + 3600, "oid": "username"}
encoded = jwt.encode(token, "secret", algorithm="HS256")
jwt_token = JWToken(encoded)
jwt_token = JWToken(encoded, key="secret", algorithms=["HS256"])

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.

We still need to have tests with the old jwt_token style, so probably it will be good to introduce another mock and have tests covering both types of token initialization.

- Use PEP 604 type hints (str | None, list[str] | None)
- Require algorithms when key is provided (raise ValueError otherwise)
- Remove verify_exp=False from both decode paths (per maintainer: don't disable expiration validation)
- Keep verify_signature=False only in backward-compat key=None path
- Add docstring describing behavior and backward-compat path
- Update tests for old init, key+algorithms, algorithms-only, and missing-algorithms error
- Add mock_identity_provider_unsafe fixture for backward-compat testing
Comment thread tests/conftest.py Outdated
Security Fix added 3 commits May 29, 2026 08:27
- Add mock_identity_provider_legacy() for backward-compat testing
- Add tests for exp=-1 sentinel with both verified and legacy init
- Add tests for audience claims with both init styles
- Addresses maintainer feedback on PR redis#4085
- Modify entraid_utils.py to support mock_idp='legacy'
- Add TestLegacyMockIdentityProvider with coverage for unverified JWToken
- Addresses maintainer feedback on PR redis#4085
@Ersa-tech

Copy link
Copy Markdown

Opened #4093 as the replacement/update from a branch I can push to with the current GitHub account. It carries the maintainer-requested coverage for the legacy token-only path, algorithms-without-key path, key+algorithms path, key-without-algorithms error, and removes the optional redis_entraid integration test path that was only added for extra coverage. Local verification on the replacement branch: focused JWT tests, Ruff, and git diff --check all pass.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 7ee2ff9. Configure here.

Comment thread redis/auth/token.py
key,
algorithms=algorithms,
options={"verify_aud": False, "verify_nbf": False},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Future nbf tokens accepted verified

Medium Severity

When key and algorithms are used, jwt.decode sets verify_nbf to false, so tokens with a future nbf still decode. The legacy path leaves default nbf checking on. is_expired() does not enforce nbf, so verified initialization can accept tokens before they are meant to be valid.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7ee2ff9. Configure here.

Comment thread redis/auth/token.py
else:
if algorithms is None:
header = jwt.get_unverified_header(self._value)
algorithms = [header["alg"]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing alg header raises KeyError

Low Severity

On the no-key path, when algorithms is omitted, the code reads header["alg"] instead of using a safe lookup. JWTs without an alg header entry raise KeyError during construction rather than failing through jwt.decode as before.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7ee2ff9. Configure here.

@petyaslavova

Copy link
Copy Markdown
Collaborator

Closing this PR since all the changes are added in #4093

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