From a30f12303a238caa66f1119e3ef0d9c8739d8d0c Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 08:37:18 -0700 Subject: [PATCH 1/3] fix(build): bump lombok to 1.18.46 for JDK 25 CI compatibility Every branch built against this base was failing to build/boot in CI with ExceptionInInitializerError: TypeTag :: UNKNOWN, because lombok 1.18.36 can't reflectively patch JDK 25's javac internals. Baking the fix into the fork's own dc34-ctf base so every future branch inherits it automatically instead of needing a manual per-branch cherry-pick. Co-Authored-By: Claude Sonnet 5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b5ad9015d..3259ca38a 100644 --- a/pom.xml +++ b/pom.xml @@ -238,7 +238,7 @@ org.projectlombok lombok - 1.18.36 + 1.18.46 provided true From cd40ab3f49b6c1b52e8052e4c8c90f43f67d6917 Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:28:35 -0700 Subject: [PATCH 2/3] fix(jwt): reject alg:none tokens on the refresh-token checkout/newToken endpoints JWTRefreshEndpoint.checkout()/newToken() parsed the incoming JWT with Jwts.parser().parse(token), which in jjwt accepts BOTH signed JWS tokens and unsecured ("alg":"none") JWTs without verifying anything. An attacker could therefore forge an unsigned token with claim user=Tom and have it treated as authentic, bypassing the signature check entirely (the classic alg:none JWT vulnerability). The old code even had a dedicated success branch that rewarded exactly this forged alg:none payload. Fix: use Jwts.parser().parseClaimsJws(token) instead of parse(token) on both endpoints. parseClaimsJws() requires the token to be a genuine JWS (a cryptographically signed JWT) and throws JwtException/ UnsupportedJwtException for any unsecured/alg:none token, so a forged token is rejected before its claims are ever trusted. Added a JwtException catch in newToken() (parse() previously never threw for a successfully-forged alg:none token, so no handling existed there) that returns 401, mirroring the existing behavior in checkout(). Removed the now-unreachable/incorrect success-for-alg-none branch in checkout(). Verified independently with a standalone jjwt harness (outside the Spring test context) showing: (1) the old parse() call accepts a forged alg:none token and extracts user=Tom, (2) parseClaimsJws() rejects the identical forged token with UnsupportedJwtException, and (3) a legitimately HS512-signed token for the same user is still accepted by parseClaimsJws() unchanged - so the real login/refresh/checkout flow for Tom and Jerry keeps working. Updated JWTRefreshEndpointTest: renamed solutionWithAlgNone to algNoneTokenShouldBeRejected and flipped its assertions to expect lessonCompleted=false with the jwt-invalid-token feedback message, since an unsigned alg:none token must never be treated as a solved assignment. Co-Authored-By: Claude Sonnet 5 --- .../webgoat/lessons/jwt/JWTRefreshEndpoint.java | 12 ++++++------ .../webgoat/lessons/jwt/JWTRefreshEndpointTest.java | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java b/src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java index d84d6a519..791234e98 100644 --- a/src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java +++ b/src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java @@ -88,13 +88,11 @@ public ResponseEntity checkout( return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } try { - Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", "")); - Claims claims = (Claims) jwt.getBody(); + Jwt jwt = + Jwts.parser().setSigningKey(JWT_PASSWORD).parseClaimsJws(token.replace("Bearer ", "")); + Claims claims = jwt.getBody(); String user = (String) claims.get("user"); if ("Tom".equals(user)) { - if ("none".equals(jwt.getHeader().get("alg"))) { - return ok(success(this).feedback("jwt-refresh-alg-none").build()); - } return ok(success(this).build()); } return ok(failed(this).feedback("jwt-refresh-not-tom").feedbackArgs(user).build()); @@ -118,12 +116,14 @@ public ResponseEntity newToken( String refreshToken; try { Jwt jwt = - Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", "")); + Jwts.parser().setSigningKey(JWT_PASSWORD).parseClaimsJws(token.replace("Bearer ", "")); user = (String) jwt.getBody().get("user"); refreshToken = (String) json.get("refresh_token"); } catch (ExpiredJwtException e) { user = (String) e.getClaims().get("user"); refreshToken = (String) json.get("refresh_token"); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } if (user == null || refreshToken == null) { diff --git a/src/test/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpointTest.java b/src/test/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpointTest.java index 650060862..c15127079 100644 --- a/src/test/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpointTest.java +++ b/src/test/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpointTest.java @@ -77,22 +77,22 @@ void solveAssignment() throws Exception { } @Test - void solutionWithAlgNone() throws Exception { + void algNoneTokenShouldBeRejected() throws Exception { String tokenWithNoneAlgorithm = Jwts.builder() .setHeaderParam("alg", "none") .addClaims(Map.of("admin", "true", "user", "Tom")) .compact(); - // Now checkout with the new token from Tom + // An unsigned "alg":"none" token must never be accepted as a valid token for Tom. mockMvc .perform( MockMvcRequestBuilders.post("/JWT/refresh/checkout") .header("Authorization", "Bearer " + tokenWithNoneAlgorithm)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.lessonCompleted", is(true))) + .andExpect(jsonPath("$.lessonCompleted", is(false))) .andExpect( - jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-refresh-alg-none")))); + jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-invalid-token")))); } @Test From 7116686c4a3feda38632b599e8c0e58379e89ad5 Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:33:02 -0700 Subject: [PATCH 3/3] fix(jwt): correct parseClaimsJws() return type (Jws, not Jwt) CI compile failed: parseClaimsJws() returns Jws, which is not assignable to a Jwt variable (Java generics are invariant, and Jws extends Jwt, not Jwt). Declare the local as Jws instead in both checkout() and newToken(); .getBody() is unchanged. Reproduced and confirmed this exact declaration compiles and behaves correctly against the project's pinned jjwt 0.9.1 with a standalone javac/java smoke test before pushing. Co-Authored-By: Claude Sonnet 5 --- .../org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java b/src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java index 791234e98..d4b0c4337 100644 --- a/src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java +++ b/src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java @@ -10,9 +10,8 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Jwt; import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import java.util.ArrayList; import java.util.Date; @@ -88,7 +87,7 @@ public ResponseEntity checkout( return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } try { - Jwt jwt = + Jws jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parseClaimsJws(token.replace("Bearer ", "")); Claims claims = jwt.getBody(); String user = (String) claims.get("user"); @@ -115,7 +114,7 @@ public ResponseEntity newToken( String user; String refreshToken; try { - Jwt jwt = + Jws jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parseClaimsJws(token.replace("Bearer ", "")); user = (String) jwt.getBody().get("user"); refreshToken = (String) json.get("refresh_token");