From b894bc0abb835f4f6f945cd6af67c1aa24cb3347 Mon Sep 17 00:00:00 2001 From: ajaysi Date: Fri, 5 Jun 2026 11:53:19 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20GHSA-426f-p74m-73fv=20=E2=80=94=20JWT=20?= =?UTF-8?q?JWKS=20issuer=20confusion=20auth=20bypass=20(CVSS=209.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin issuer and JWKS URL at startup from CLERK_PUBLISHABLE_KEY. Validate token iss claim before any JWKS fetch. Add issuer= to jwt.decode() with verify_iss=True. --- backend/middleware/auth_middleware.py | 49 +++++++++++++++++---------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/backend/middleware/auth_middleware.py b/backend/middleware/auth_middleware.py index 743584fe..16a30899 100644 --- a/backend/middleware/auth_middleware.py +++ b/backend/middleware/auth_middleware.py @@ -50,6 +50,7 @@ class ClerkAuthMiddleware: # Cache for PyJWKClient to avoid repeated JWKS fetches self._jwks_client_cache = {} self._jwks_url_cache = None + self._issuer_cache = None # Pre-configured Clerk issuer for iss validation if not self.clerk_secret_key and not self.disable_auth: logger.warning("CLERK_SECRET_KEY not found, authentication may fail") @@ -58,15 +59,16 @@ class ClerkAuthMiddleware: if CLERK_AUTH_AVAILABLE and not self.disable_auth: try: if self.clerk_secret_key and self.clerk_publishable_key: - # Extract instance from publishable key for JWKS URL + # Extract instance from publishable key for JWKS URL and issuer validation # Format: pk_test_. or pk_live_. parts = self.clerk_publishable_key.replace('pk_test_', '').replace('pk_live_', '').split('.') if len(parts) >= 1: # Extract the domain from publishable key or use default # Clerk URLs are typically: https://.clerk.accounts.dev instance = parts[0] - jwks_url = f"https://{instance}.clerk.accounts.dev/.well-known/jwks.json" - + issuer_url = f"https://{instance}.clerk.accounts.dev" + jwks_url = f"{issuer_url}/.well-known/jwks.json" + # Create Clerk configuration with JWKS URL clerk_config = ClerkConfig( secret_key=self.clerk_secret_key, @@ -76,6 +78,7 @@ class ClerkAuthMiddleware: self.clerk_bearer = ClerkHTTPBearer(clerk_config) logger.info(f"fastapi-clerk-auth initialized successfully with JWKS URL: {jwks_url}") self._jwks_url_cache = jwks_url + self._issuer_cache = issuer_url # Pin issuer for VULN-001 fix else: logger.warning("Could not extract instance from publishable key") self.clerk_bearer = None @@ -118,19 +121,29 @@ class ClerkAuthMiddleware: import jwt from jwt import PyJWKClient - # Get the JWKS URL from the token header + # Get the unverified header for key ID lookup unverified_header = jwt.get_unverified_header(token) - - # Decode token to get issuer for JWKS URL + + # --- SECURITY FIX (VULN-001): Validate issuer before any JWKS fetch --- + # Pre-configured issuer and JWKS URL derived from CLERK_PUBLISHABLE_KEY + # NEVER use the token's 'iss' claim to construct the JWKS URL (GHSA-426f-p74m-73fv) + expected_issuer = self._issuer_cache + jwks_url = self._jwks_url_cache + if not expected_issuer or not jwks_url: + raise Exception("Clerk issuer/JWKS URL not configured at startup") + + # Decode token to validate the issuer claim against the pre-configured value + # WARNING: We must first validate 'iss' before trusting anything else unverified_claims = jwt.decode(token, options={"verify_signature": False}) - issuer = unverified_claims.get('iss', '') - - # Construct JWKS URL from issuer - jwks_url = f"{issuer}/.well-known/jwks.json" if issuer else self._jwks_url_cache or "" - if not jwks_url: - raise Exception("Unable to resolve JWKS URL for Clerk verification") - - # Use cached PyJWKClient to avoid repeated JWKS fetches + token_issuer = unverified_claims.get('iss', '') + if token_issuer != expected_issuer: + logger.error( + f"Issuer mismatch: token claims '{token_issuer}' " + f"but expected '{expected_issuer}'" + ) + return None + + # Use cached PyJWKClient with pinned jwks_url (never derived from token) if jwks_url not in self._jwks_client_cache: logger.info(f"Creating new PyJWKClient for {jwks_url} with caching enabled") # Create client with caching enabled (cache_keys=True keeps keys in memory) @@ -139,17 +152,19 @@ class ClerkAuthMiddleware: cache_keys=True, max_cached_keys=16 ) - + jwks_client = self._jwks_client_cache[jwks_url] signing_key = jwks_client.get_signing_key_from_jwt(token) - + # Verify and decode the token with clock skew tolerance # Add 300 seconds (5 minutes) leeway to handle clock skew and token refresh delays + # SECURITY: Always pass issuer= to verify the token's 'iss' matches expected (VULN-001) decoded_token = jwt.decode( token, signing_key.key, algorithms=["RS256"], - options={"verify_signature": True, "verify_exp": True}, + issuer=expected_issuer, + options={"verify_signature": True, "verify_exp": True, "verify_iss": True}, leeway=300 # Allow 5 minutes leeway for token refresh during navigation )