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.searchResults") @@ -104,7 +112,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) + "

" + 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.wellDone") - + "

" - + "

" - + 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 = "

" + bundle.getString("response.searchResults") + "

"