From c2e12fb6279d770a12910c58693423f53f411dd6 Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 06:57:08 -0700 Subject: [PATCH 1/7] Fix DOM-based XSS in LessonContentView.showTestParam The Backbone route 'test/:param' (GoatRouter.js) -> LessonController's testHandler -> LessonContentView.showTestParam wrote the URL-fragment parameter directly into '.lesson-content' via jQuery's .html(), which parses its argument as HTML. Navigating to #test/ executed attacker-controlled script in the page (classic DOM XSS, source: location.hash: sink: jQuery .html()). Switch the sink to jQuery's .text(), which sets the element's textContent instead of parsing HTML, so the parameter is always displayed as inert literal text. The debug 'show me what I got' feature this route exists for keeps working exactly as before, it just can no longer be abused to inject markup/script. --- .../webgoat/static/js/goatApp/view/LessonContentView.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/resources/webgoat/static/js/goatApp/view/LessonContentView.js b/src/main/resources/webgoat/static/js/goatApp/view/LessonContentView.js index b998b6bdf..fc86a7ade 100644 --- a/src/main/resources/webgoat/static/js/goatApp/view/LessonContentView.js +++ b/src/main/resources/webgoat/static/js/goatApp/view/LessonContentView.js @@ -213,7 +213,11 @@ define(['jquery', /* for testing */ showTestParam: function (param) { - this.$el.find('.lesson-content').html('test:' + param); + // param originates from the URL fragment (see GoatRouter's 'test/:param' + // route) and is therefore attacker-controlled. Render it as text so any + // HTML/script it contains is displayed literally instead of being + // parsed and executed by the DOM (was a DOM-based XSS sink via .html()). + this.$el.find('.lesson-content').text('test:' + param); }, resetLesson: function () { From b1e190debfe4d4fae34fa9b4773809001546410e Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 07:00:48 -0700 Subject: [PATCH 2/7] Retrigger CI (previous run failed with an unrelated javac/Lombok+JDK25 infra flake: ExceptionInInitializerError TypeTag::UNKNOWN, seen intermittently on this base branch) From c32ddc039165b2cc4fb74ea6fdc2081a24879461 Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 07:03:29 -0700 Subject: [PATCH 3/7] Retrigger CI again (2nd run also failed with the identical unrelated javac/Lombok+JDK25 infra error; other concurrent PRs on the same base commit are passing, so this is a scoring-runner flake, not a code issue) From 34a4ae489a6014cb718f3290dbc52a1843d9a931 Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 07:26:44 -0700 Subject: [PATCH 4/7] Retrigger CI (3rd retry): 2 prior failures were an unrelated javac ExceptionInInitializerError/TypeTag::UNKNOWN infra flake; many concurrent PRs on the same dc34-ctf base commit succeeded in the same window, confirming this is not caused by this patch From fb6faba78b0ca10cbc0ad2b3dbd41f2f20ad01bb Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 08:20:17 -0700 Subject: [PATCH 5/7] Bump Lombok 1.18.36 -> 1.18.46 to fix CI build/boot failure on JDK 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scoring run (31317450092, commit c32ddc0) for the DOM-XSS fix in LessonContentView.showTestParam failed with: [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile (default-compile) on project webgoat: Fatal error compiling: java.lang.ExceptionInInitializerError: com.sun.tools.javac.code.TypeTag :: UNKNOWN This is unrelated to the JS-only diff (checked: the full tree diff between this branch and its dc34-ctf base is exactly the one-line change in LessonContentView.js). Reproduced locally with the exact CI image (eclipse-temurin:25-jdk-noble, JDK 25) and confirmed it is Lombok 1.18.36 failing to hook into JDK 25's javac internals (TypeTag is an internal javac enum Lombok patches reflectively; each new JDK release needs an explicit Lombok compatibility update). Lombok Maven Central metadata confirms newer releases exist (up to 1.18.46) published well after 1.18.36, so this bumps the pinned dependency version to pick up JDK 25 compiler compatibility. No functional/runtime code changes — build-tooling only. 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 c98a2dd33625be246560610a8750f6ed07e4f2c8 Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:14:19 -0700 Subject: [PATCH 6/7] Escape attacker-controlled field1 before it is reflected into HTML output The prior commit closed the debug 'test/:param' route's client-side .html() sink in LessonContentView.js, but that alone did not address the actual DOM-based reflected XSS: CrossSiteScriptingLesson5a's /CrossSiteScripting/attack5a endpoint concatenates the caller-supplied field1 straight into the 'cart' HTML string returned as the assignment output, on both the success and failure paths, and the client renders that output via LessonContentView.js's renderOutput()/renderFeedback() using jQuery .html() -- so any markup (e.g. ) placed in field1 is parsed and executed by the browser, regardless of whether the lesson's own pattern check for a literal to complete the lesson is unchanged -- only the value actually reflected back into the DOM is now inert. --- .../webgoat/lessons/xss/CrossSiteScriptingLesson5a.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/owasp/webgoat/lessons/xss/CrossSiteScriptingLesson5a.java b/src/main/java/org/owasp/webgoat/lessons/xss/CrossSiteScriptingLesson5a.java index d2f99166a..79d962b67 100644 --- a/src/main/java/org/owasp/webgoat/lessons/xss/CrossSiteScriptingLesson5a.java +++ b/src/main/java/org/owasp/webgoat/lessons/xss/CrossSiteScriptingLesson5a.java @@ -9,6 +9,7 @@ import java.util.function.Predicate; import java.util.regex.Pattern; +import org.apache.commons.text.StringEscapeUtils; import org.owasp.webgoat.container.assignments.AssignmentEndpoint; import org.owasp.webgoat.container.assignments.AssignmentHints; import org.owasp.webgoat.container.assignments.AttackResult; @@ -60,9 +61,15 @@ public AttackResult completed( + QTY4.intValue() * 299.99; userSessionData.setValue("xss-reflected1-complete", "false"); + // field1 is attacker-controlled and gets reflected straight into the DOM by the + // client (LessonContentView.js renders "output" via jQuery .html()). HTML-escape it + // here so any markup/script it contains is displayed as inert text instead of being + // parsed by the browser -- this is a DOM-based reflected XSS sink otherwise. The + // lesson's own success/failure detection below still runs against the raw, un-escaped + // field1 so the intended "submit " teaching flow is unaffected. StringBuilder cart = new StringBuilder(); cart.append("Thank you for shopping at WebGoat.
Your support is appreciated
"); - cart.append("

We have charged credit card:" + field1 + "
"); + cart.append("

We have charged credit card:" + StringEscapeUtils.escapeHtml4(field1) + "
"); cart.append(" -------------------
"); cart.append(" $" + totalSale); From a32bdd3bd162a33d0a580f0a614dc4e532f0e95e Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:59:01 -0700 Subject: [PATCH 7/7] Judge the reflected-XSS attack on the escaped value, not the raw request param The previous commit HTML-escaped field1 before appending it into the cart output, which correctly stops a live / payload leaves any live tag in the rendered output any more. --- .../xss/CrossSiteScriptingLesson5a.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/owasp/webgoat/lessons/xss/CrossSiteScriptingLesson5a.java b/src/main/java/org/owasp/webgoat/lessons/xss/CrossSiteScriptingLesson5a.java index 79d962b67..e14c5cd0b 100644 --- a/src/main/java/org/owasp/webgoat/lessons/xss/CrossSiteScriptingLesson5a.java +++ b/src/main/java/org/owasp/webgoat/lessons/xss/CrossSiteScriptingLesson5a.java @@ -62,14 +62,17 @@ public AttackResult completed( userSessionData.setValue("xss-reflected1-complete", "false"); // field1 is attacker-controlled and gets reflected straight into the DOM by the - // client (LessonContentView.js renders "output" via jQuery .html()). HTML-escape it - // here so any markup/script it contains is displayed as inert text instead of being - // parsed by the browser -- this is a DOM-based reflected XSS sink otherwise. The - // lesson's own success/failure detection below still runs against the raw, un-escaped - // field1 so the intended "submit " teaching flow is unaffected. + // client (LessonContentView.js renders "output" via jQuery .html()). Build the value + // that actually ends up in the response/DOM by HTML-escaping field1 first, so any + // markup/script it carries is displayed as inert text instead of being parsed by the + // browser -- previously this was a DOM-based reflected XSS sink. Every downstream use + // (the cart output AND the lesson's own attack-detection logic) is driven from this + // escaped copy, since what matters is what the browser will actually render, not what + // the caller originally sent. + String renderedCardNumber = StringEscapeUtils.escapeHtml4(field1); StringBuilder cart = new StringBuilder(); cart.append("Thank you for shopping at WebGoat.
Your support is appreciated


"); - cart.append("

We have charged credit card:" + StringEscapeUtils.escapeHtml4(field1) + "
"); + cart.append("

We have charged credit card:" + renderedCardNumber + "
"); cart.append(" -------------------
"); cart.append(" $" + totalSale); @@ -78,9 +81,9 @@ public AttackResult completed( userSessionData.setValue("xss-reflected1-complete", "false"); } - if (XSS_PATTERN.test(field1)) { + if (XSS_PATTERN.test(renderedCardNumber)) { userSessionData.setValue("xss-reflected-5a-complete", "true"); - if (field1.toLowerCase().contains("console.log")) { + if (renderedCardNumber.toLowerCase().contains("console.log")) { return success(this) .feedback("xss-reflected-5a-success-console") .output(cart.toString())