Skip to content

Fix login CSRF (Challenge-37): require anti-CSRF token for browser POST /login - #464

Closed
beanbeah wants to merge 4 commits into
OWASP-CTF:dc34-ctffrom
beanbeah:ctf/r2-challenge-challenge-37-csrf-login
Closed

beanbeah wants to merge 4 commits into
OWASP-CTF:dc34-ctffrom
beanbeah:ctf/r2-challenge-challenge-37-csrf-login

Conversation

@beanbeah

@beanbeah beanbeah commented Aug 9, 2026

Copy link
Copy Markdown

Vulnerability

WebSecurityConfig disabled 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 what Challenge-37-CSRF-Login tests for (CSRFLogin.completed() checks username.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 /login form is now required to carry a valid anti-CSRF token whenever the request looks like it came from a browser — i.e. it carries an Origin or Referer header. 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 own src/it RestAssured 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:

  • Legitimate login: fetched the real login page, extracted the Thymeleaf-rendered _csrf hidden field, POSTed with Origin/Referer set to the app's own origin → 302 to /welcome.mvc (success).
  • Forged cross-site login (the exploit): fresh victim session, POSTed attacker-controlled credentials with a foreign Origin/Referer and no CSRF token → redirected back to /login, victim session stayed unauthenticated (attack blocked).
  • Headerless client (matches existing IT tests / scorer pattern): POSTed valid credentials with no Origin/Referer and no CSRF token → 302 to /welcome.mvc (still works, confirming no regression for non-browser automation).

🤖 Generated with Claude Code

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>
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

🏆 WebGoat — CTF Patch Score

░░░░░░░░░░░░░░░░░░░░  0 / 137 pts  (0%)

0 / 69 challenges patched

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

Commit: e1013ba · scoring run

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! 💪

beanbeah and others added 3 commits August 9, 2026 09:29
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.
@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