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 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() {