From 84ca3000bc7722d39f32fe6dfe81dfd0feb2812e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 22 Apr 2026 10:10:26 +0000
Subject: [PATCH 1/2] Initial plan
From b1011f5028498d606e1dbd47bcd7d35c75d567fa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 22 Apr 2026 10:15:56 +0000
Subject: [PATCH 2/2] fix secure passwords assignment validation escaping and
reuse
Agent-Logs-Url: https://github.com/Cluster-Mesh/WebGoat/sessions/26e5a9a1-fcde-49ed-81ce-73ba5f49f1ab
Co-authored-by: GGuerraReply <133228566+GGuerraReply@users.noreply.github.com>
---
.../SecurePasswordsAssignment.java | 41 ++++++++++------
.../SecurePasswordsAssignmentTest.java | 48 +++++++++++++++++++
2 files changed, 74 insertions(+), 15 deletions(-)
create mode 100644 src/test/java/org/owasp/webgoat/lessons/securepasswords/SecurePasswordsAssignmentTest.java
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:");
for (String sug : strength.getFeedback().getSuggestions())
- output.append("- " + sug + "
");
+ output.append("- " + HtmlUtils.htmlEscape(sug) + "
");
output.append("
");
}
- 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");
+ }
+}