This is an automated email from the ASF dual-hosted git repository.
jongyoul pushed a commit to branch branch-0.12
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/branch-0.12 by this push:
new ea9b2466e3 [ZEPPELIN-6626] Fix JWT expiration validation security
vulnerability
ea9b2466e3 is described below
commit ea9b2466e364741a5b7c0fb2615c08ee8eeee57c
Author: Dongmin Cha <[email protected]>
AuthorDate: Tue Aug 5 19:28:58 2025 +0900
[ZEPPELIN-6626] Fix JWT expiration validation security vulnerability
### What is this PR for?
This PR fixes a critical security vulnerability in JWT token validation
where tokens without expiration time were incorrectly accepted as valid,
potentially allowing indefinite unauthorized access. The
fix ensures all JWT tokens must have a valid expiration time and adds
comprehensive unit tests to prevent regression.
### What type of PR is it?
Bug Fix
### Todos
* [x] - Fix JWT expiration validation logic to reject null expiration
tokens
* [x] - Add security warning logs for rejected tokens
* [x] - Create comprehensive unit tests for JWT validation scenarios
* [x] - Verify backwards compatibility with existing valid tokens
### What is the Jira issue?
* [ZEPPELIN-6266](https://issues.apache.org/jira/browse/ZEPPELIN-6266)
Fix JWT expiration validation security vulnerability
### How should this be tested?
* **Automated Unit Tests Added**:
- New test file: `KnoxJwtRealmTest.java` with 3 comprehensive test
scenarios
- Run: `mvn test -Dtest=KnoxJwtRealmTest -pl zeppelin-server`
- All tests pass: `Tests run: 3, Failures: 0, Errors: 0, Skipped: 0`
* **Manual Testing Steps**:
1. Create JWT token without expiration time → Should be rejected with
security warning
2. Create JWT token with valid future expiration → Should be accepted
3. Create JWT token with past expiration → Should be rejected
4. Check server logs for security warnings: "JWT token has no
expiration time - rejecting token for security"
* **Security Validation**:
- Verify that tokens without expiration are properly rejected
- Confirm existing valid tokens continue to work
- Check security event logging
### Screenshots (if appropriate)
N/A - Security fix with no UI changes
### Questions:
* **Does the license files need to update?** No - only modified existing
files and added test files with standard Apache license headers
* **Is there breaking changes for older versions?** No - fully backwards
compatible. Only affects tokens with missing expiration (which should not exist
in proper JWT implementations)
* **Does this needs documentation?** No - internal security fix that
doesn't change public APIs or configuration
Closes #5007 from chadongmin/ZEPPELIN-6266.
Signed-off-by: Jongyoul Lee <[email protected]>
(cherry picked from commit 4bf5ab99d32b00c2d80ce7c77d27aecebf58c81d)
Signed-off-by: Jongyoul Lee <[email protected]>
---
.../apache/zeppelin/realm/jwt/KnoxJwtRealm.java | 10 +-
.../zeppelin/realm/jwt/KnoxJwtRealmTest.java | 102 +++++++++++++++++++++
2 files changed, 108 insertions(+), 4 deletions(-)
diff --git
a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java
b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java
index 3a036889e6..0a1d8decc9 100644
---
a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java
+++
b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java
@@ -192,10 +192,12 @@ public class KnoxJwtRealm extends AuthorizingRealm {
boolean valid = false;
try {
Date expires = jwtToken.getJWTClaimsSet().getExpirationTime();
- if (expires == null || new Date().before(expires)) {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("SSO token expiration date has been " + "successfully
validated");
- }
+ if (expires == null) {
+ LOGGER.warn("JWT token has no expiration time - rejecting token for
security");
+ return false;
+ }
+ if (new Date().before(expires)) {
+ LOGGER.debug("SSO token expiration date has been successfully
validated");
valid = true;
} else {
LOGGER.warn("SSO expiration date validation failed.");
diff --git
a/zeppelin-server/src/test/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealmTest.java
b/zeppelin-server/src/test/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealmTest.java
new file mode 100644
index 0000000000..d9860f9107
--- /dev/null
+++
b/zeppelin-server/src/test/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealmTest.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zeppelin.realm.jwt;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class KnoxJwtRealmTest {
+
+ private KnoxJwtRealm knoxJwtRealm;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ knoxJwtRealm = new KnoxJwtRealm();
+ }
+
+ private SignedJWT createJWT(String subject, Date expiration) throws
Exception {
+ JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder()
+ .subject(subject)
+ .issuer("KNOXSSO")
+ .issueTime(new Date());
+
+ if (expiration != null) {
+ claimsBuilder.expirationTime(expiration);
+ }
+
+ JWTClaimsSet claimsSet = claimsBuilder.build();
+ SignedJWT signedJWT = new SignedJWT(
+ new JWSHeader.Builder(JWSAlgorithm.RS256).build(),
+ claimsSet
+ );
+
+ // Note: We don't sign the JWT for these tests as we're only testing
expiration validation
+ return signedJWT;
+ }
+
+ @Test
+ void testValidateExpiration_WithNullExpiration_ShouldReturnFalse() throws
Exception {
+ // Given: JWT token without expiration time
+ SignedJWT jwtWithoutExpiration = createJWT("testuser", null);
+
+ // When: validating expiration
+ boolean result = knoxJwtRealm.validateExpiration(jwtWithoutExpiration);
+
+ // Then: should return false
+ assertFalse(result, "JWT token without expiration time should be
rejected");
+ }
+
+ @Test
+ void testValidateExpiration_WithValidFutureExpiration_ShouldReturnTrue()
throws Exception {
+ // Given: JWT token with future expiration
+ Date futureDate = new Date(System.currentTimeMillis() + 3600000); // 1
hour from now
+ SignedJWT jwtWithValidExpiration = createJWT("testuser", futureDate);
+
+ // When: validating expiration
+ boolean result = knoxJwtRealm.validateExpiration(jwtWithValidExpiration);
+
+ // Then: should return true
+ assertTrue(result, "JWT token with valid future expiration should be
accepted");
+ }
+
+ @Test
+ void testValidateExpiration_WithPastExpiration_ShouldReturnFalse() throws
Exception {
+ // Given: JWT token with past expiration
+ Date pastDate = new Date(System.currentTimeMillis() - 3600000); // 1 hour
ago
+ SignedJWT jwtWithPastExpiration = createJWT("testuser", pastDate);
+
+ // When: validating expiration
+ boolean result = knoxJwtRealm.validateExpiration(jwtWithPastExpiration);
+
+ // Then: should return false
+ assertFalse(result, "JWT token with past expiration should be rejected");
+ }
+
+ // Note: Full token validation tests are omitted as they require complex
setup
+ // including certificate files and signature validation. The core expiration
+ // validation logic is tested above through direct method calls.
+}