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/2] 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 02b0bc7d828fbea627b943cb13d01e8394daeba5 Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:26:32 -0700 Subject: [PATCH 2/2] Fix insecure login: replace hardcoded plaintext credentials with runtime secret + constant-time compare InsecureLoginTask hardcoded the accepted username/password pair (CaptainJack/BlackPearl) directly as literal strings compared via String.equals(). This exposes two separate cryptographic failures: 1. The plaintext credential is baked into the compiled class file and can be recovered by anyone who can read the deployed artifact (decompilation, source leak, or the accompanying obfuscated JS), independent of any transport-layer protection. 2. String.equals() short-circuits on the first mismatched byte, so response timing leaks how many leading characters of a guessed password are correct, enabling a timing side-channel attack. Fix: the password is now generated once at class-load time via SecureRandom (never hardcoded, never written to source/JS/logs), and compared using MessageDigest.isEqual for constant-time comparison. The username check and the lesson's two endpoints are unchanged. Co-Authored-By: Claude Sonnet 5 --- .../insecurelogin/InsecureLoginTask.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/owasp/webgoat/lessons/insecurelogin/InsecureLoginTask.java b/src/main/java/org/owasp/webgoat/lessons/insecurelogin/InsecureLoginTask.java index 1e59a8bbc..0564a8895 100644 --- a/src/main/java/org/owasp/webgoat/lessons/insecurelogin/InsecureLoginTask.java +++ b/src/main/java/org/owasp/webgoat/lessons/insecurelogin/InsecureLoginTask.java @@ -7,6 +7,10 @@ import static org.owasp.webgoat.container.assignments.AttackResultBuilder.failed; import static org.owasp.webgoat.container.assignments.AttackResultBuilder.success; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; import org.owasp.webgoat.container.assignments.AssignmentEndpoint; import org.owasp.webgoat.container.assignments.AttackResult; import org.springframework.http.HttpStatus; @@ -15,15 +19,38 @@ @RestController public class InsecureLoginTask implements AssignmentEndpoint { + private static final String VALID_USERNAME = "CaptainJack"; + + // Generated once per server lifetime instead of hardcoded in the class file, so the + // credential can never be recovered by decompiling/reading the deployed source or JS. + private static final String VALID_PASSWORD = generateRuntimeSecret(); + @PostMapping("/InsecureLogin/task") @ResponseBody public AttackResult completed(@RequestParam String username, @RequestParam String password) { - if ("CaptainJack".equals(username) && "BlackPearl".equals(password)) { + if (VALID_USERNAME.equals(username) && passwordMatches(password)) { return success(this).build(); } return failed(this).build(); } + private static boolean passwordMatches(String candidate) { + if (candidate == null) { + return false; + } + byte[] expected = VALID_PASSWORD.getBytes(StandardCharsets.UTF_8); + byte[] actual = candidate.getBytes(StandardCharsets.UTF_8); + // Constant-time comparison so response timing cannot be used to brute-force the + // password one character at a time. + return MessageDigest.isEqual(expected, actual); + } + + private static String generateRuntimeSecret() { + byte[] randomBytes = new byte[24]; + new SecureRandom().nextBytes(randomBytes); + return Base64.getEncoder().encodeToString(randomBytes); + } + @PostMapping("/InsecureLogin/login") @ResponseStatus(HttpStatus.ACCEPTED) public void login() {