diff --git a/src/main/java/org/owasp/webgoat/lessons/securepasswords/SecurePasswordsAssignment.java b/src/main/java/org/owasp/webgoat/lessons/securepasswords/SecurePasswordsAssignment.java index b402b6380d8..a8f471265d8 100644 --- a/src/main/java/org/owasp/webgoat/lessons/securepasswords/SecurePasswordsAssignment.java +++ b/src/main/java/org/owasp/webgoat/lessons/securepasswords/SecurePasswordsAssignment.java @@ -18,28 +18,37 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.HtmlUtils; @RestController public class SecurePasswordsAssignment implements AssignmentEndpoint { + private static final Zxcvbn ZXCVBN = new Zxcvbn(); + @PostMapping("SecurePasswords/assignment") @ResponseBody public AttackResult completed(@RequestParam String password) { - Zxcvbn zxcvbn = new Zxcvbn(); + if (password == null || password.trim().isEmpty()) { + return failed(this) + .feedback("securepassword-failed") + .output("Error: Password must not be empty.") + .build(); + } + StringBuilder output = new StringBuilder(); DecimalFormat df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); df.setMaximumFractionDigits(340); - Strength strength = zxcvbn.measure(password); + Strength strength = ZXCVBN.measure(password); output.append("Your Password: *******
"); - output.append("Length: " + password.length() + "
"); + output.append("Length: " + HtmlUtils.htmlEscape(String.valueOf(password.length())) + "
"); output.append( "Estimated guesses needed to crack your password: " - + df.format(strength.getGuesses()) + + HtmlUtils.htmlEscape(df.format(strength.getGuesses())) + "
"); output.append( "
Score: " - + strength.getScore() + + HtmlUtils.htmlEscape(String.valueOf(strength.getScore())) + "/4
"); if (strength.getScore() <= 1) { output.append( @@ -56,24 +65,26 @@ public AttackResult completed(@RequestParam String password) { } output.append( "Estimated cracking time: " - + calculateTime( - (long) strength.getCrackTimeSeconds().getOnlineNoThrottling10perSecond()) + + HtmlUtils.htmlEscape( + calculateTime( + (long) strength.getCrackTimeSeconds().getOnlineNoThrottling10perSecond())) + "
"); - output.append( - "Note: This estimate assumes brute-force attack and does not account for " - + "dictionary or rule-based attacks, which can significantly reduce real-world cracking time " - + "for common phrases.
"); + output.append( + "Note: This estimate assumes brute-force attack and does not account for " + + "dictionary or rule-based attacks, which can significantly reduce real-world cracking time " + + "for common phrases.
"); if (strength.getFeedback().getWarning().length() != 0) - output.append("Warning: " + strength.getFeedback().getWarning() + "
"); + output.append( + "Warning: " + HtmlUtils.htmlEscape(strength.getFeedback().getWarning()) + "
"); // possible feedback: https://github.com/dropbox/zxcvbn/blob/master/src/feedback.coffee // maybe ask user to try also weak passwords to see and understand feedback? if (strength.getFeedback().getSuggestions().size() != 0) { output.append("Suggestions:

"); } - output.append("Score: " + strength.getScore() + "/4
"); + output.append("Score: " + HtmlUtils.htmlEscape(String.valueOf(strength.getScore())) + "/4
"); if (strength.getScore() >= 4) return success(this).feedback("securepassword-success").output(output.toString()).build(); @@ -91,7 +102,7 @@ public static String calculateTime(long seconds) { long days = (seconds % yr) / (d); long hours = (seconds % d) / (hr); long minutes = (seconds % hr) / (min); - long sec = (seconds % min * s); + long sec = seconds % min; return (years + " years " diff --git a/src/test/java/org/owasp/webgoat/lessons/securepasswords/SecurePasswordsAssignmentTest.java b/src/test/java/org/owasp/webgoat/lessons/securepasswords/SecurePasswordsAssignmentTest.java new file mode 100644 index 00000000000..9feb0753a27 --- /dev/null +++ b/src/test/java/org/owasp/webgoat/lessons/securepasswords/SecurePasswordsAssignmentTest.java @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: Copyright © 2026 WebGoat authors + * SPDX-License-Identifier: GPL-2.0-or-later + */ +package org.owasp.webgoat.lessons.securepasswords; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.owasp.webgoat.container.assignments.AttackResult; + +class SecurePasswordsAssignmentTest { + + private final SecurePasswordsAssignment assignment = new SecurePasswordsAssignment(); + + @Test + void shouldFailWhenPasswordIsNull() { + AttackResult result = assignment.completed(null); + + assertThat(result.assignmentSolved()).isFalse(); + assertThat(result.getFeedback()).isEqualTo("securepassword-failed"); + assertThat(result.getOutput()).contains("Password must not be empty."); + } + + @Test + void shouldFailWhenPasswordIsBlank() { + AttackResult result = assignment.completed(" "); + + assertThat(result.assignmentSolved()).isFalse(); + assertThat(result.getFeedback()).isEqualTo("securepassword-failed"); + assertThat(result.getOutput()).contains("Password must not be empty."); + } + + @Test + void shouldMaskPasswordInResponseOutput() { + String password = "VeryStrongPassword!123"; + AttackResult result = assignment.completed(password); + + assertThat(result.getOutput()).contains("Your Password: *******"); + assertThat(result.getOutput()).doesNotContain(password); + } + + @Test + void calculateTimeShouldReturnExpectedSecondsRemainder() { + assertThat(SecurePasswordsAssignment.calculateTime(61)) + .isEqualTo("0 years 0 days 0 hours 1 minutes 1 seconds"); + } +}