R
RivenX173
Guest
TL;DR: On August 11, 2026, I was one lab away from finishing PortSwigger’s CSRF module when the final lab, “SameSite Lax bypass via cookie refresh,” refused to start. PortSwigger support told me recent Chrome updates to the victim browser had broken it, and within 24 hours, they had quietly removed both the lab and the theory section titled “Bypassing SameSite Lax restrictions with newly issued cookies,” from the Academy entirely. I accepted that explanation for a while. Then, weeks later, I went looking for any public record of Chrome actually removing this two-minute grace window, and found nothing. So I built a two-site Python proof-of-concept and tested it across 8 Chrome versionsto find out for myself whether the bypass still exists.
Short answer: it does.
While I was working my way through PortSwigger’s Web Security Academy, I reached the CSRF section. On August 11, I was almost done with it, with only one lab left: SameSite Lax bypass via cookie refresh. Every time I tried to launch it, I got the same error:
After trying several times with no luck, I decided to email PortSwigger support at [email protected]. Five hours later, I got a reply: the issue was caused by recent updates to the victim’s browser, which was now running Chrome 150. As a result, the lab was no longer solvable on modern browsers, so it had been disabled and would be retired.
Even though I’m someone who doesn’t take things at face value and usually prefers to dig for evidence myself, I let my guard down this time. I told myself: PortSwigger obviously knows their own labs and their own vulnerabilities better than I do, so if they said that, it must be true. Less than 24 hours later, the lab had been completely removed from the platform, dropping the total lab count from 274 to 273. Along with it, they also deleted the entire theory section titled “Bypassing SameSite Lax restrictions with newly issued cookies” from inside the Bypassing SameSite cookie restrictions topic, itself part of the CSRF topic.
I completely forgot about the deleted lab until the end of August, when a question crossed my mind: why not actually look up the news about this vulnerability being removed from modern browsers? The first surprise was that there was nothing to find, no mention anywhere of this behavior being removed from Chrome. So I told myself, fine, let’s just go straight to the source: the official Chrome 150 release notes. I went through them line by line, and there wasn’t a single mention of any change to cookie behavior, let alone to this two-minute grace window specifically.
Before I go further, it’s worth giving a quick overview of the vulnerability itself, specifically the deleted section on which the lab depended.
Cross-Site Request Forgery (CSRF) is a vulnerability that tricks a logged-in victim’s browser into sending a request to a target site without their knowledge or consent, using the victim’s own session cookies. Because the browser automatically attaches cookies to requests made to the site that issued them, an attacker-controlled page can trigger a request to the vulnerable site that the target server has no way of distinguishing from a genuine, user-initiated action. If the target site doesn’t have proper anti-CSRF protections, like CSRF tokens or strict cookie policies, this can let an attacker perform actions on the victim’s behalf, such as changing an email address or transferring funds.
One of the main modern defenses against this is the
This is exactly why
If you want the full technical breakdown, PortSwigger’s own write-up on CSRF covers it properly.
For anyone who wants to see the deleted section for themselves in full, titled “Bypassing SameSite Lax restrictions with newly issued cookies,” you can find an archived copy from May 12, 2026, here on the Wayback Machine. And if you visit PortSwigger now, or view any archived copy of the “Bypassing SameSite cookie restrictions” topic on the Wayback Machine after Aug 12, 2026, you will not find that deleted section, like this archived copy from Sep 10, 2026, here. You can also find an archived copy of the deleted lab itself, “SameSite Lax bypass via cookie refresh,” from May 14, 2026, on the same site, here.Ruling Out the Boring Explanation
At this point, everything I had was still just speculation. Maybe there was a simple, boring explanation: the two-minute window had been a known, temporary quirk of Chrome for years, ever since it was first introduced, which is exactly why Chrome never felt the need to call it out in a release note, and why no other site mentioned it either.
But as I like to say, “it happens because it happens” never satisfies me.
So in early September, I made myself a cup of my favorite black coffee and sat down to build a simple proof-of-concept, as far as my abilities would allow, to settle, once and for all, whether this vulnerability still existed or not.
I put together two Python files,
At first, both files were simple, just a few dozen lines of Python, and something I hadn’t expected actually happened: the attack succeeded, and the cookie was sent within the first 120 seconds of its creation, in a POST request resulting from a top-level navigation. That result demanded a pause. Up until that moment I genuinely didn’t believe this was real; if anything, I assumed I had to be the one making a mistake. So to push this as far as I reasonably could, a bare-bones script with a handful of lines wasn’t going to cut it; I had to develop it further to make the results as reliable as possible.
Let me first explain how the two scripts work together.
All you need to do is open two terminal windows and run one script in each, using regular Python commands (use
and keep both terminal windows open.
Then, switch to Chrome and open:
To make this easier to follow, think of
Together, these two sites can simulate several scenarios, which I’ll walk through one by one below.
If the victim first visits
You’ll find plenty of detail on the page, but clicking the purple “Send Normal Cross-Site POST request” button, which simulates attempting to send a cross-site POST request, results in failure: no cookie is sent. That’s simply because the victim isn’t logged in to
Naturally, if the victim instead clicks “Refresh Cookie & Send Cross-Site POST request,” the result is the same, since you can’t refresh a cookie value that doesn’t exist yet.
If the victim first visits
You’ll notice two separate timers:
You can also check
If the victim then visits
If the victim visits
If the victim visits
To run this, the victim needs to first visit
The result ends up identical to Scenario 4, with no difference at all.
As you’ve probably noticed, the five scenarios above don’t really resemble real-world CSRF attacks, since in practice there are no buttons for the victim to click. So I tried to simulate that more realistically with a “Real World Scenario Mode” toggle, which is set to OFF the first time you run the scripts.
Turning it on switches it straight to green:
The idea behind this toggle is simple: once it’s turned on, after just 5 seconds, it automatically performs the exact same action as the “Send Normal Cross-Site POST request” button, sending a cross-site POST request with whatever cookie currently exists.
On top of that, if you click anywhere on the
Or the cookie not being sent with the request after a refresh, but where the
Neither of these two scenarios has actually occurred, though, so they’re not expected to show up unless someone deliberately tampers with the cookies in the browser, editing or deleting them manually.
Once I’d finished this experiment, I reached out to PortSwigger again on September 6 to share what I’d found. I genuinely wanted to know why they removed the lab and the theory section, because maybe I had done something in my testing that compromised the accuracy of the results. All possibilities were on the table.
I got a reply from them on September 8 confirming that my request had been passed on to the developers.
Two weeks went by with no further response, so I sent a follow-up on September 22 and got one last reply the same day. They said they had indeed forwarded my request to the development team, but couldn’t currently give me a specific date for when they’d get back to me.
In any case, if I get any update from them, I’ll edit this write-up accordingly.
And to be clear, I’m not saying PortSwigger did anything wrong here. Disabling a specific lab is their call to make, even if the vulnerability still exists, and they surely have their reasons. But removing the breakdown of a browser-behavior bypass that my testing shows is still active, across 8 Chrome versions, is a separate question I can’t answer. And if there’s a real takeaway for me in all of this, it’s simpler than any of that: whenever something nags at your curiosity, go verify the explanation yourself, even when there’s already something that looks like proof sitting in front of you. Rerun it yourself.
More generally, if anyone finds a meaningful issue in the scripts that leads to inaccurate results, please let me know in the comments or on GitHub, as long as it’s backed by practical evidence and isn’t just a stylistic suggestion for the script itself. I’m not a developer and still fairly new to this field, so mistakes are entirely possible. This entire write-up could turn out to be based on something I simply missed. But all I can really say is that this whole thing happened only because “it happens because it happens” never satisfies me.
Short answer: it does.
While I was working my way through PortSwigger’s Web Security Academy, I reached the CSRF section. On August 11, I was almost done with it, with only one lab left: SameSite Lax bypass via cookie refresh. Every time I tried to launch it, I got the same error:
Apologies, the lab could not be started in a timely manner. Please try again or contact us if the problem persists.
After trying several times with no luck, I decided to email PortSwigger support at [email protected]. Five hours later, I got a reply: the issue was caused by recent updates to the victim’s browser, which was now running Chrome 150. As a result, the lab was no longer solvable on modern browsers, so it had been disabled and would be retired.
Even though I’m someone who doesn’t take things at face value and usually prefers to dig for evidence myself, I let my guard down this time. I told myself: PortSwigger obviously knows their own labs and their own vulnerabilities better than I do, so if they said that, it must be true. Less than 24 hours later, the lab had been completely removed from the platform, dropping the total lab count from 274 to 273. Along with it, they also deleted the entire theory section titled “Bypassing SameSite Lax restrictions with newly issued cookies” from inside the Bypassing SameSite cookie restrictions topic, itself part of the CSRF topic.
The Beginning
I completely forgot about the deleted lab until the end of August, when a question crossed my mind: why not actually look up the news about this vulnerability being removed from modern browsers? The first surprise was that there was nothing to find, no mention anywhere of this behavior being removed from Chrome. So I told myself, fine, let’s just go straight to the source: the official Chrome 150 release notes. I went through them line by line, and there wasn’t a single mention of any change to cookie behavior, let alone to this two-minute grace window specifically.
Some Background: What Is CSRF?
Before I go further, it’s worth giving a quick overview of the vulnerability itself, specifically the deleted section on which the lab depended.
Cross-Site Request Forgery (CSRF) is a vulnerability that tricks a logged-in victim’s browser into sending a request to a target site without their knowledge or consent, using the victim’s own session cookies. Because the browser automatically attaches cookies to requests made to the site that issued them, an attacker-controlled page can trigger a request to the vulnerable site that the target server has no way of distinguishing from a genuine, user-initiated action. If the target site doesn’t have proper anti-CSRF protections, like CSRF tokens or strict cookie policies, this can let an attacker perform actions on the victim’s behalf, such as changing an email address or transferring funds.
One of the main modern defenses against this is the
SameSite cookie attribute, and specifically its Lax setting, which is Chrome's default when no SameSite attribute is explicitly set:Lax SameSite restrictions mean that browsers will send the cookie in cross-site requests, but only if both of the following conditions are met:- The request uses the GET method.
- The request results from a top-level navigation by the user, such as clicking on a link.
This is exactly why
Lax is supposed to block CSRF attacks that rely on cross-site POST requests, the kind most CSRF attacks actually need. But there's a documented exception to this rule, and it's the exact piece PortSwigger deleted, since it's what the entire lab, and this write-up, are built on. In short: if a site doesn't set a SameSite attribute on a cookie, Chrome defaults to Lax, but to avoid breaking single sign-on (SSO) flows, it doesn't actually enforce those Lax restrictions for the first 120 seconds after the cookie is issued, even on top-level POST requests. That leaves a two-minute window where a freshly issued cookie can still be sent cross-site. This grace period only applies to cookies that get Lax by default; it doesn't apply to cookies explicitly set with SameSite=Lax. Since it's impractical to time an attack around a random login, the documented trick was to find a gadget on the site, something like an OAuth-based SSO login, that causes the victim's existing cookie to be refreshed with a new value, then follow up immediately with the CSRF request while that refreshed cookie is still inside its two-minute window.If you want the full technical breakdown, PortSwigger’s own write-up on CSRF covers it properly.
For anyone who wants to see the deleted section for themselves in full, titled “Bypassing SameSite Lax restrictions with newly issued cookies,” you can find an archived copy from May 12, 2026, here on the Wayback Machine. And if you visit PortSwigger now, or view any archived copy of the “Bypassing SameSite cookie restrictions” topic on the Wayback Machine after Aug 12, 2026, you will not find that deleted section, like this archived copy from Sep 10, 2026, here. You can also find an archived copy of the deleted lab itself, “SameSite Lax bypass via cookie refresh,” from May 14, 2026, on the same site, here.Ruling Out the Boring Explanation
At this point, everything I had was still just speculation. Maybe there was a simple, boring explanation: the two-minute window had been a known, temporary quirk of Chrome for years, ever since it was first introduced, which is exactly why Chrome never felt the need to call it out in a release note, and why no other site mentioned it either.
But as I like to say, “it happens because it happens” never satisfies me.
So in early September, I made myself a cup of my favorite black coffee and sat down to build a simple proof-of-concept, as far as my abilities would allow, to settle, once and for all, whether this vulnerability still existed or not.
Building the Proof of Concept
I put together two Python files,
site_a and site_b. One acts as the victim site holding the cookie, and the other acts as the attacker's site. I'll walk through exactly how this PoC works below, and I'll share the repository for anyone who wants to try it themselves.At first, both files were simple, just a few dozen lines of Python, and something I hadn’t expected actually happened: the attack succeeded, and the cookie was sent within the first 120 seconds of its creation, in a POST request resulting from a top-level navigation. That result demanded a pause. Up until that moment I genuinely didn’t believe this was real; if anything, I assumed I had to be the one making a mistake. So to push this as far as I reasonably could, a bare-bones script with a handful of lines wasn’t going to cut it; I had to develop it further to make the results as reliable as possible.
How the Scripts Work
Let me first explain how the two scripts work together.
All you need to do is open two terminal windows and run one script in each, using regular Python commands (use
python or python3 depending on your environment):
Code:
python site_a.py
python site_b.py
and keep both terminal windows open.
Then, switch to Chrome and open:
http://127.0.0.1:8000 for site_a, and http://localhost:9000 for site_b.To make this easier to follow, think of
site_b, which holds the cookie, as a banking website, and site_a as a malicious site controlled by an attacker, like one of those fake ad sites, trying to fire off a CSRF request against the victim's bank account.Together, these two sites can simulate several scenarios, which I’ll walk through one by one below.
- Note: Both scripts are available on GitHub.
Scenario 1: What if the attacker tricks the victim into sending a cross-site POST request while they aren’t logged in at all (no cookie exists yet)?
If the victim first visits
site_a at http://127.0.0.1:8000:
You’ll find plenty of detail on the page, but clicking the purple “Send Normal Cross-Site POST request” button, which simulates attempting to send a cross-site POST request, results in failure: no cookie is sent. That’s simply because the victim isn’t logged in to
site_b, so no cookie exists in the first place.
site_b's terminal will show the following:
Code:
Received cross-site POST request from Site A
Cookie header received : None
Does it contain 'test='? : No
Age since FIRST ever issued : N/A
Age since LAST (re)issued : N/A
Naturally, if the victim instead clicks “Refresh Cookie & Send Cross-Site POST request,” the result is the same, since you can’t refresh a cookie value that doesn’t exist yet.
Scenario 2: What if the attacker tricks the victim into sending a cross-site POST request right after they’ve just logged in, i.e., less than 120 seconds have passed since the cookie was created?
If the victim first visits
site_b at http://localhost:9000:
You’ll notice two separate timers:
Since FIRST ever issued:, which tracks the time since the very first cookie was ever created and isn't affected by subsequent refreshes, and Since LAST (re)issued, which tracks the time since the most recent refresh of the cookie's value.You can also check
F12 -> Application -> Cookies to find it listed under the name test, along with its value.If the victim then visits
site_a and clicks "Send Normal Cross-Site POST request," the result is a successful cookie delivery along with the request.
site_b's terminal will show the following:
Code:
Received cross-site POST request from Site A
Cookie header received : test=80e48c4274680018
Does it contain 'test='? : Yes
Age since FIRST ever issued : 15.5s
Age since LAST (re)issued : 15.5s
Scenario 3: What if the attacker tricks the victim into sending a cross-site POST request more than 120 seconds after the cookie was created?
If the victim visits
site_a and clicks "Send Normal Cross-Site POST request" after more than 120 seconds have passed since the cookie was created, the result is a failed delivery; the cookie is not sent with the request.
site_b's terminal will show the following:
Code:
Received cross-site POST request from Site A
Cookie header received : None
Does it contain 'test='? : No
Age since FIRST ever issued : 558.4s
Age since LAST (re)issued : 558.4s
Scenario 4: What if the attacker tricks the victim into refreshing a cookie that’s older than 120 seconds, then immediately sends a cross-site POST request with it?
If the victim visits
site_a and clicks "Refresh Cookie & Send Cross-Site POST request" after more than 120 seconds have passed since the cookie was originally created, the result is a small pop-up window appearing in the bottom-right corner. Right after that, the cookie gets refreshed and sent successfully with the request. This means refreshing the cookie's value resets the 120-second grace window entirely, regardless of when the cookie was originally created. You'll also notice that the Since FIRST ever issued: timer does not reset when the cookie value is refreshed.
site_b's terminal will show the following:
Code:
[SET] Cookie 'test' (re)issued
Cookie value : d9b9caba320fc725
Time since FIRST ever issued : 899.4s
Time since LAST (re)issued : 0.0s
Set-Cookie header sent? : Yes
Received cross-site POST request from Site A
Cookie header received : test=d9b9caba320fc725
Does it contain 'test='? : Yes
Age since FIRST ever issued : 899.8s
Age since LAST (re)issued : 0.4s
Scenario 5: What if the victim refreshes their own cookie, one that’s older than 120 seconds, before the attacker even tricks them into sending the cross-site POST request?
To run this, the victim needs to first visit
site_b and click "Refresh Cookie Now," then go to site_a and click "Send Normal Cross-Site POST request."The result ends up identical to Scenario 4, with no difference at all.
- Note: even though this scenario isn’t realistic, since a victim has no reason to refresh their own cookie, I was simply curious whether there’d be any difference between the victim refreshing it themselves versus the attacker tricking them into doing so.
A More Realistic Scenario
As you’ve probably noticed, the five scenarios above don’t really resemble real-world CSRF attacks, since in practice there are no buttons for the victim to click. So I tried to simulate that more realistically with a “Real World Scenario Mode” toggle, which is set to OFF the first time you run the scripts.
Turning it on switches it straight to green:
The idea behind this toggle is simple: once it’s turned on, after just 5 seconds, it automatically performs the exact same action as the “Send Normal Cross-Site POST request” button, sending a cross-site POST request with whatever cookie currently exists.
On top of that, if you click anywhere on the
site_a page before those 5 seconds run out, it instead performs the exact same action as the "Refresh Cookie & Send Cross-Site POST request" button: a pop-up appears to refresh the cookie, followed immediately by a cross-site POST request carrying the newly refreshed cookie.- Note: in a real attack, an attacker wouldn’t actually combine both behaviors like this; they’d pick one or the other, either sending a plain request or forcing a cookie refresh first. I only combined them here because I wanted a single feature that could simulate a realistic attack flow end to end.
Caveats and Limitations
- I tested these scripts across 8 different versions of Chrome, and got the same result every time:
141.0.7340.0/142.0.7444.60/144.0.7559.133/149.0.7827.201/150.0.7871.47/152.0.7977.83/154.0.8037.98/155.0.8059.40. That said, the scripts don't hardcode or check for any specific Chrome version; they simply test the browser's live behavior at the time you run them. Feel free to run them against whatever version of Chrome you currently have installed. - Chrome treats
http://localhost:9000andhttp://127.0.0.1:8000as entirely different origins, so the test isn't affected by this in any case. Still, as a precaution, I also edited the OShostsfile and ran each script onsite-a.comandsite-b.cominstead. There was no difference in the results. - The cookie-refresh step relies on opening a pop-up window, and browsers normally block pop-ups that get triggered automatically, without any direct user interaction. That’s never an issue in the scripts, though: in the regular scenarios, the pop-up only fires when the victim clicks the “Refresh Cookie & Send Cross-Site POST request” button themselves, and in Real World Scenario Mode, it only fires when the victim clicks anywhere on the
site_apage. Either way, there's always a genuine click from the victim behind it, so the browser never has a reason to block it. - Occasionally, something breaks when navigating between
site_bandsite_ausing the buttons. It's rare, but I'll be honest: I don't know the cause, so I wasn't able to fix it. - The scripts are full of detailed notes explaining every result, along with several buttons to make navigation and testing smoother. Feel free to explore them yourself while running the PoC.
- If you want to restart the experiment from scratch, I don’t recommend restarting the browser and terminals, as stale cookies can still linger in the browser and affect the new test. Instead, use the ‘Close the Site & Go to Site A’ or ‘Clear & Reset’ buttons, depending on what you need, since they properly clear the relevant data.
- I don’t recommend manually editing or deleting the cookies from inside the browser on
site_b, since the scripts weren't built to handle that, and doing so will produce incorrect results. - These scripts were also built to handle edge cases that haven’t actually occurred yet in Chrome, for example, a cookie not being sent with the request even though fewer than 120 seconds have passed since its creation:
site_b's terminal would show the following:
Code:
Received cross-site POST request from Site A
Cookie header received : None
Does it contain 'test='? : No
Age since FIRST ever issued : 14.8s
Age since LAST (re)issued : 14.8s
Or the cookie not being sent with the request after a refresh, but where the
Since FIRST ever issued counter has already passed 120 seconds, which would mean the 120-second window does not reset when the cookie value is refreshed:
site_b's terminal would show the following:
Code:
Received cross-site POST request from Site A
Cookie header received : None
Does it contain 'test='? : No
Age since FIRST ever issued : 146.2s
Age since LAST (re)issued : 11.7s
Neither of these two scenarios has actually occurred, though, so they’re not expected to show up unless someone deliberately tampers with the cookies in the browser, editing or deleting them manually.
Reaching Out to PortSwigger Again
Once I’d finished this experiment, I reached out to PortSwigger again on September 6 to share what I’d found. I genuinely wanted to know why they removed the lab and the theory section, because maybe I had done something in my testing that compromised the accuracy of the results. All possibilities were on the table.
I got a reply from them on September 8 confirming that my request had been passed on to the developers.
Two weeks went by with no further response, so I sent a follow-up on September 22 and got one last reply the same day. They said they had indeed forwarded my request to the development team, but couldn’t currently give me a specific date for when they’d get back to me.
In any case, if I get any update from them, I’ll edit this write-up accordingly.
And to be clear, I’m not saying PortSwigger did anything wrong here. Disabling a specific lab is their call to make, even if the vulnerability still exists, and they surely have their reasons. But removing the breakdown of a browser-behavior bypass that my testing shows is still active, across 8 Chrome versions, is a separate question I can’t answer. And if there’s a real takeaway for me in all of this, it’s simpler than any of that: whenever something nags at your curiosity, go verify the explanation yourself, even when there’s already something that looks like proof sitting in front of you. Rerun it yourself.
More generally, if anyone finds a meaningful issue in the scripts that leads to inaccurate results, please let me know in the comments or on GitHub, as long as it’s backed by practical evidence and isn’t just a stylistic suggestion for the script itself. I’m not a developer and still fairly new to this field, so mistakes are entirely possible. This entire write-up could turn out to be based on something I simply missed. But all I can really say is that this whole thing happened only because “it happens because it happens” never satisfies me.
Timeline
- Aug 11, 2026: Discovered the lab wasn’t working.
- Aug 11, 2026: Sent the first message to PortSwigger.
- Aug 11, 2026: Received a reply from PortSwigger.
- Aug 12, 2026: PortSwigger removed the lab and the theory section titled “Bypassing SameSite Lax restrictions with newly issued cookies.”
- Sep 01 — Sep 05, 2026: Built the initial scripts and tested them across multiple Chrome versions.
- Sep 06, 2026: Reached out to PortSwigger again.
- Sep 08, 2026: Received a reply from PortSwigger confirming my request had been forwarded to the development team.
- Sep 22, 2026: Sent a follow-up to PortSwigger.
- Sep 22, 2026: Received a reply from PortSwigger stating they couldn’t give me a specific date for a response.
- Oct 01 — Oct 09, 2026: Refined the scripts and expanded testing across additional newer Chrome versions.