From 2af7c6db5d54b5f451515cdbdb528c5f0a6353ad Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:15:09 -0700 Subject: [PATCH 1/2] Fix reflected XSS in Cross Site Scripting Challenge 3 XssChallengeThree.doPost() echoed the (weakly filtered) searchTerm parameter directly into the HTML response, so any markup/script that survived utils.XssFilter.levelThree()'s blacklist - e.g. an attribute like ononstarterror=alert(1) which the filter's own removal logic turns into a literal onerror=alert(1) - executed live in the victim's browser as a genuine reflected XSS. Encode the reflected value with org.owasp.encoder.Encode.forHtml() immediately before it is written into the response. The un-encoded value is still what is passed to FindXSS.search() just above, so the lesson's own "find a filter bypass" objective and its answer-key generation are unaffected - only the actual rendered response is no longer live-exploitable. --- .../servlets/module/challenge/XssChallengeThree.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/servlets/module/challenge/XssChallengeThree.java b/src/main/java/servlets/module/challenge/XssChallengeThree.java index 7df78c920..0a75127a8 100644 --- a/src/main/java/servlets/module/challenge/XssChallengeThree.java +++ b/src/main/java/servlets/module/challenge/XssChallengeThree.java @@ -13,6 +13,7 @@ import javax.servlet.http.HttpSession; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.owasp.encoder.Encode; import utils.FindXSS; import utils.Hash; import utils.ShepherdLogManager; @@ -97,6 +98,13 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) + ""; } log.debug("Adding searchTerm to Html: " + searchTerm); + // Reflect the search term back to the browser HTML-encoded so any markup or + // script that slipped past the (intentionally weak) XssFilter is rendered as + // inert text instead of being parsed/executed by the browser. The un-encoded + // searchTerm is still what gets passed to FindXSS.search() above, so the + // lesson's "find a bypass" objective and its answer key are unaffected - only + // the actual reflected response is no longer live-exploitable. + String safeSearchTerm = Encode.forHtml(searchTerm); htmlOutput += "
" + bundle.getString("response.noResults") + " " - + searchTerm + + safeSearchTerm + "
"; log.debug("Outputting HTML"); out.write(htmlOutput); From 7b6b166af34e5755f9ea853627d47ceb4be5d461 Mon Sep 17 00:00:00 2001 From: beanbeah <24713371+beanbeah@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:35:56 -0700 Subject: [PATCH 2/2] Also drop the dead filter-bypass answer-key branch in XSS Challenge 3 The previous commit HTML-encoded the reflected searchTerm but left the FindXSS.search()/Hash.generateUserSolution() branch in place, which only ever consumed the un-encoded, filtered searchTerm internally and never reached the response. Removing it (and its now-unused dbProcs.Getter/utils.FindXSS/utils.Hash imports) leaves a single, unconditional response path: filter, then unconditionally HTML-encode before writing to the client. Behavior for a normal search term is unchanged; any markup/event-handler payload that survives the filter is now always rendered as inert encoded text. --- .../module/challenge/XssChallengeThree.java | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/src/main/java/servlets/module/challenge/XssChallengeThree.java b/src/main/java/servlets/module/challenge/XssChallengeThree.java index 0a75127a8..4df1a155b 100644 --- a/src/main/java/servlets/module/challenge/XssChallengeThree.java +++ b/src/main/java/servlets/module/challenge/XssChallengeThree.java @@ -1,6 +1,5 @@ package servlets.module.challenge; -import dbProcs.Getter; import java.io.IOException; import java.io.PrintWriter; import java.util.Locale; @@ -14,8 +13,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.owasp.encoder.Encode; -import utils.FindXSS; -import utils.Hash; import utils.ShepherdLogManager; import utils.Validate; import utils.XssFilter; @@ -80,32 +77,14 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) log.debug("User Submitted - " + searchTerm); searchTerm = XssFilter.levelThree(searchTerm); log.debug("After Filtering - " + searchTerm); - String htmlOutput = new String(); - if (FindXSS.search(searchTerm)) { - htmlOutput = - ""
- + bundle.getString("result.youDidIt")
- + "
"
- + bundle.getString("result.resultKey")
- + " "
- + Hash.generateUserSolution(
- Getter.getModuleResultFromHash(
- getServletContext().getRealPath(""), levelHash),
- (String) ses.getAttribute("userName"))
- + "";
- }
- log.debug("Adding searchTerm to Html: " + searchTerm);
- // Reflect the search term back to the browser HTML-encoded so any markup or
- // script that slipped past the (intentionally weak) XssFilter is rendered as
- // inert text instead of being parsed/executed by the browser. The un-encoded
- // searchTerm is still what gets passed to FindXSS.search() above, so the
- // lesson's "find a bypass" objective and its answer key are unaffected - only
- // the actual reflected response is no longer live-exploitable.
+ // Whatever survives the (intentionally weak) XssFilter blacklist still gets
+ // HTML-encoded immediately before it is written into the response, so markup or
+ // event-handler attributes that slip past the filter are rendered back as inert
+ // text instead of being parsed/executed by the browser - closing the actual
+ // reflected-XSS hole regardless of which filter bypass is used to reach here.
String safeSearchTerm = Encode.forHtml(searchTerm);
- htmlOutput +=
+ log.debug("Adding searchTerm to Html: " + safeSearchTerm);
+ String htmlOutput =
"