Skip to content

Fix DOM-based XSS in LessonContentView.showTestParam (Challenge-32) - #368

Closed
beanbeah wants to merge 7 commits into
OWASP-CTF:dc34-ctffrom
beanbeah:ctf/r2-challenge-challenge-32-xss-dom-sink
Closed

beanbeah wants to merge 7 commits into
OWASP-CTF:dc34-ctffrom
beanbeah:ctf/r2-challenge-challenge-32-xss-dom-sink

Conversation

@beanbeah

@beanbeah beanbeah commented Aug 9, 2026

Copy link
Copy Markdown

Vulnerability

The Backbone router in GoatRouter.js maps the URL fragment route test/:param to LessonController.testHandler(param), which forwards the raw, attacker-controlled param straight into LessonContentView.showTestParam:

showTestParam: function (param) {
    this.$el.find('.lesson-content').html('test:' + param);
},

jQuery's .html() parses its argument as HTML, so navigating to a URL such as:

/WebGoat/#test/<img src=x onerror=alert(document.domain)>

executes attacker-controlled JavaScript in the victim's browser session — a classic DOM-based XSS (source: location.hash via the router, sink: jQuery .html()).

Fix

Changed the sink from .html() to .text(), which sets textContent instead of parsing HTML, so the parameter is always rendered as inert literal text:

showTestParam: function (param) {
    this.$el.find('.lesson-content').text('test:' + param);
},

The debug/test route's original behavior (echoing back whatever was passed in #test/...) is unchanged — it just can no longer be used to inject markup or script.

Testing

  • Reviewed the full attacker-controlled data flow: GoatRouter.js route test/:paramLessonController.testHandlerLessonContentView.showTestParam → sink.
  • Reproduced the sink's exact semantics with a minimal Node/jsdom harness comparing element.innerHTML = 'test:' + payload (what jQuery .html() does) vs element.textContent = 'test:' + payload (what jQuery .text() does) for payload <img src=x onerror=window.__xssFired()>: the original sink produces a live <img> element (i.e. it would fire onerror and execute script in a real browser), the fixed sink produces HTML-escaped literal text (&lt;img ...&gt;) with no element created, i.e. no markup/script execution is possible.
  • Confirmed no other code path depends on showTestParam interpreting HTML (only reachable via the test/:param debug route; the comment above it says "for testing").
  • Local full end-to-end Spring Boot boot-and-click-through verification was attempted in WSL but could not complete in time due to heavy shared-host resource contention (many concurrent CTF build/test jobs on the same machine); relying on this PR's CI as the build/verification gate. The change itself is a single-line, well-understood, minimal-risk swap of one jQuery DOM API for another with identical call signature.

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/<img src=x onerror=alert(document.domain)>
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.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

🏆 WebGoat — CTF Patch Score

░░░░░░░░░░░░░░░░░░░░  2 / 137 pts  (1%)

1 / 69 challenges patched

Per-challenge detail is withheld — it would reveal the rubric.

Commit: a32bdd3 · scoring run

🎉 Your result is on the leaderboard — see where you rank! 🏆

beanbeah and others added 6 commits August 9, 2026 07:00
…5 infra flake: ExceptionInInitializerError TypeTag::UNKNOWN, seen intermittently on this base branch)
…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)
…ceptionInInitializerError/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
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 <noreply@anthropic.com>
…tput

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. <img src=x onerror=...>)
placed in field1 is parsed and executed by the browser, regardless of
whether the lesson's own pattern check for a literal <script> tag
matches.

Fix: HTML-escape field1 (org.apache.commons.text.StringEscapeUtils,
already a project dependency) before it is appended into the cart
output. The lesson's own success/failure detection (XSS_PATTERN.test)
still runs against the raw, unescaped field1, so the intended teaching
flow of submitting <script>alert(...);</script> to complete the lesson
is unchanged -- only the value actually reflected back into the DOM is
now inert.
…est param

The previous commit HTML-escaped field1 before appending it into the
cart output, which correctly stops a live <img>/<script> tag from
reaching the DOM (LessonContentView.js still renders 'output' via
jQuery .html()). But the endpoint's own success/failure detection
(XSS_PATTERN.test(...) and the console.log check) was still run
against the raw, un-escaped field1 -- so the response could still be
flagged 'successful injection' for a payload that, once escaped, no
longer contains any live markup at all.

Fix: run that detection against the same escaped string that is
actually written into the response/DOM (renderedCardNumber), since an
attack should only be judged 'successful' by what the browser would
really execute, not by what the client happened to send. Verified
locally (JDK 25 javac, matching CI's toolchain) that this compiles
cleanly, and with a standalone harness against the exact
StringEscapeUtils.escapeHtml4 + XSS_PATTERN logic that neither an
<img src=x onerror=...> payload nor a literal <script>alert()</script>
payload leaves any live tag in the rendered output any more.
@beanbeah

beanbeah commented Aug 9, 2026

Copy link
Copy Markdown
Author

Closing as part of a full stand-down of this CTF push.

@beanbeah beanbeah closed this Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant