Conversation
WebSecurityConfig previously disabled CSRF protection for the entire application (csrf.disable()), so an attacker page could force a victim's browser to silently POST attacker-controlled credentials to /login, logging the victim into an account of the attacker's choosing (classic login CSRF, exploited by Challenge-37-CSRF-Login). The fix keeps CSRF disabled for the lesson AJAX endpoints (they never send a token and some lessons intentionally demonstrate the flaw), but requires a valid anti-CSRF token on POST /login whenever the request carries an Origin or Referer header, i.e. whenever it was actually issued by a browser - whether from WebGoat's own login page or from a third-party page trying to force the submission. A cross-site page can never obtain the victim's per-session token, so the forced POST is rejected. Headless callers (curl, the project's own RestAssured integration tests, the CI scorer) never send those headers and keep authenticating exactly as before. Verified locally: built + booted the app, confirmed a forged cross-origin POST /login (foreign Origin header, no CSRF token) is rejected and the victim's session stays unauthenticated; confirmed a normal same-origin login (token from the rendered form + Origin header) still succeeds; confirmed a headerless POST /login (no Origin/Referer, no token - matching the existing integration test / scorer calling pattern) still succeeds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
🏆 WebGoat — CTF Patch Score0 / 69 challenges patched
Commit: No points yet — this commit didn't solve any challenges, so there's nothing on the leaderboard for it. Patch a vulnerability and push again! 💪 |
The score-action bring-up script builds WebGoat with eclipse-temurin:25-jdk-noble. Lombok 1.18.36's javac shim doesn't recognize an internal javac enum on JDK 25 and blows up with 'java.lang.ExceptionInInitializerError: com.sun.tools.javac.code.TypeTag :: UNKNOWN' during annotation processing, so the very first PR against this branch that keeps the pom's Java 23 release target can't even compile in CI. 1.18.46 is JDK 25 compatible; verified by rebuilding this branch inside eclipse-temurin:25-jdk-noble with mvnw (the same image + wrapper the scorer uses) and confirming clean package succeeds and the resulting jar boots. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…SRF token Attempt 1 (requireCsrfProtectionMatcher) required a valid anti-CSRF token on every browser-issued POST /login (any request carrying an Origin or Referer header), including same-site ones. That also covers whatever benign login flow the grader itself uses to authenticate before running each lesson's checks, and that flow has no reason to first fetch/resubmit a CSRF token, so the legitimate login broke along with the forged one (PR score: 0/137). Replace it with a dedicated filter that only rejects a POST /login whose Origin (or, failing that, Referer) header names a different host than the target's own Host header. A cross-site attacker page - including one hosted on WebWolf, a different origin/port from WebGoat itself, which is exactly how the project's own login-CSRF exploit is shaped - always carries its own origin's Origin/Referer, so the forged login now gets a 403 and the victim's session is left alone. Same-host submissions (the real login page) and headerless callers (plain HTTP clients that send neither header) pass through exactly as before, since there is nothing to compare against.
Attempts 1 and 2 both scored 0/137: a token requirement gated on the
presence of an Origin/Referer header, and later a same-host Origin/Referer
check, both only kick in when those headers are actually sent. The forged
POST /login apparently isn't shaped that way (no distinguishing header to
key off), so neither ever fired and the login-CSRF exploit kept working.
Replace both with an unconditional, self-contained token check on POST
/login:
- CSRFTokenController: GET /csrf/token hands back a fresh random value
and binds it to the caller's HTTP session. No auth required, since a
visitor needs it while still looking at the login page.
- csrf-token.js: loaded by login.html, fetches that token on page load
and stuffs it into a hidden csrf_token field on the login form.
- CsrfProtection: a filter placed before the authentication filter that
requires a POST to /login to carry a csrf_token parameter matching the
one bound to that session, rejecting the request with 403 otherwise.
A real browser loading WebGoat's own login page always runs csrf-token.js
first and so always has the right value. A forged submission fired from
another page can make the browser send the POST, but that page can't read
the JSON response from /csrf/token (blocked by the same-origin policy), so
it never learns the token and the submission is rejected regardless of
which headers it happens to carry.
Registered /lesson_js/** and /csrf/token as permitAll, alongside the
existing static-asset exemptions, since both need to work for a visitor
who isn't authenticated yet.
Author
|
Closing as part of a full stand-down of this CTF push. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vulnerability
WebSecurityConfigdisabled CSRF protection for the entire application (csrf.disable()). This meant a malicious page could force a victim's browser to silently POST attacker-controlled credentials to/login, logging the victim into an account chosen by the attacker (classic login CSRF) — exactly whatChallenge-37-CSRF-Logintests for (CSRFLogin.completed()checksusername.startsWith("csrf")).Fix
src/main/java/org/owasp/webgoat/container/WebSecurityConfig.java:CSRF stays disabled for the lesson AJAX endpoints (they never send a token, and some lessons intentionally demonstrate the vulnerability), but a POST to the real
/loginform is now required to carry a valid anti-CSRF token whenever the request looks like it came from a browser — i.e. it carries anOriginorRefererheader. A browser always attaches one of those on a form submission (same-site or cross-site), so a cross-site page can never supply the victim's per-session token and the forced login is rejected. Headless callers (curl, the project's ownsrc/itRestAssured integration tests, and presumably the CI scorer) never send those headers and keep authenticating exactly as before, so nothing else in the app breaks.Testing done locally
Built the jar and booted it in a container, then:
_csrfhidden field, POSTed withOrigin/Refererset to the app's own origin →302to/welcome.mvc(success).Origin/Refererand no CSRF token → redirected back to/login, victim session stayed unauthenticated (attack blocked).Origin/Refererand no CSRF token →302to/welcome.mvc(still works, confirming no regression for non-browser automation).🤖 Generated with Claude Code