May 29, 2026
Cracking SameSite for a $2,000 Web Cache Deception
Akwaaba! everyone. This one was quite fun, and I will breakdown my methodology. My last writeup was a $500 OAuth Account Fusion bug which…
tinopreter
7 min read
Akwaaba! everyone. This one was quite fun, and I will breakdown my methodology. My last writeup was a $500 OAuth Account Fusion bug which was easy. Rate Limit shouldn't stop you, Bypass it with Special Headers.
‼️ Disclaimer: I've changed specific details about the application in this write-up for confidentiality. Even the screenshots shown are from a different, similar application that doesn't have a bug bounty program — they're only used for illustration. Also, all API endpoints mentioned have been altered to avoid revealing the actual program.
The Program
I recently received an invitation to test a booking platform. The platform supports self-signup and is hosted entirely on cloud infrastructure. Upon authenticating, the application handles user sessions by passing a JWT directly inside the Cookie header.
The Vulnerability
I discovered a Web Cache Deception (WCD) vulnerability that allowed an attacker to trick victims into caching their sensitive account details. This happened because the application embedded user-specific information directly into a page that was configured to always cache. Once cached, an attacker could simply retrieve those private details from the public cache URL to take over the victim's account.
Before diving into the exploitation steps, I will quickly brush over how cloud infrastructure caching works, what Web Cache Deception is, and how it differs from Web Cache Poisoning. If you are already familiar with these concepts, feel free to skip straight to the "How I Identified the bug" section.
Last time, I escalated a $500 into a $1,500 Email Verification Bypass payout
Caching
In modern cloud infrastructure, when you send a request to an application server, it almost always passes through an intermediary (such as a reverse proxy, CDN, or load balancer) before reaching its final destination. The same thing happens on the way back to your browser.
Cachingis a mechanism where an intermediary proxy stores a copy of the original response from the backend server. When subsequent users request that exact same resource, the proxy serves the stored copy directly to them, eliminating the need to query the application server a second time.
You can tell a response is cached by looking at its X-Cache headers.
A value of MISS means you are getting a fresh response directly from the origin server, while a HIT means you are being served a cached copy from the intermediary.
Web Cache Deception (and why it happens + how to spot it)
Imagine requesting an endpoint like /my-profile. The resulting HTML page loads your personal details (like your email and home address) directly into the source code. Because this page contains private, user-specific data, it should never be cached.
WCD occurs when this sensitive page mistakenly gets cached. Once stored by the proxy, if an attacker requests that same endpoint, they won't get a fresh response; instead, they are served your cached page, exposing your private details.
Web Cache Poisoning (WCP)
While WCD results in a sensitive page being cached and exposed to other users, WCP works differently.
With WCP, an attacker finds a page that is cached and uses an injection attack (like XSS) to modify its content. This "poisoned" version of the page is then stored in the cache. When subsequent users request the page, they are served the malicious version, causing the attacker's payload to execute directly in their browsers.
$1,500 Field-level vulns are common inGraphQL APIs. Here's how to find them.
How Identified the Bug
I authenticated and got redirected to the homepage. I normally do a quick overview of the HTML response before moving on to check the JS files that got loaded. I noticed an X-Cache: MISS header and a Cache-Control header that didn't specify private. This meant the homepage was eligible for caching, and the MISS indicated that the first response was coming straight from the server to be stored by the cache.
I sent the request to Repeater and resent it. This time, it returned X-Cache: HIT, confirming I was now being served the cached version of the response.
Now, caching an application's homepage is completely normal. Most apps do it, and the reasoning makes sense. For most platforms, the homepage displays the exact same static content for every single visitor. It is only user-specific pages like
/my-profileor/settingsthat contain private data, which normally don't get cached (and absolutely shouldn't be cached).
So, the app caching the homepage wasn't a vulnerability on its own. But, looking closer at that cached response, I found the currently authenticated user's JWT embedded directly inside the page's JavaScript tags.
Now, this is a vulnerability. If you are going to cache a response, you must ensure that absolutely no user-specific or sensitive data is included in it.
Read my proven $1,500 Recon Tips for beginners also
How I Exploited this
Now that I knew the homepage included the user's JWT in its response and that the entire response was being cached, the exploit path was clear. I just had to wait for another user to visit the homepage, let the proxy cache their response, and then visit the page myself to extract their cached JWT.
With millions of people using the app, users were constantly overwriting the cache. As old caches expired, the next person to load the page immediately became the new victim. However, to make this a targeted attack rather than a random lottery, I needed to use a cache buster.
A cache buster is a unique or randomized parameter appended to a URL (like ?cb=123) that forces the caching server to treat the request as entirely new, rather than serving a previously cached copy. By controlling the cache buster, I could isolate a specific session.
To perform the targeted attack, I constructed the following malicious URL:
https://target.com/?cacheBuster=1https://target.com/?cacheBuster=1If an authenticated victim visits this URL, the homepage loads normally for them, and they can continue using the platform. However, behind the scenes, their response is now securely cached by the proxy. As the attacker, I just have to visit that exact same URL to be served the victim's cached response, allowing me to easily copy their JWT right from the source code.
Forming a Payload
To deliver the malicious link to my victim, I could either embed it in an email or host it on an attacker-controlled site. I chose the latter approach.
<!DOCTYPE html>
<html>
<head>
<title>WCD PoC</title>
</head>
<body>
<h3>Redirecting for PoC…</h3>
<!-- auto request the URL-->
<img src="https://www.target.com/?cacheBuster=1">
</body>
</html><!DOCTYPE html>
<html>
<head>
<title>WCD PoC</title>
</head>
<body>
<h3>Redirecting for PoC…</h3>
<!-- auto request the URL-->
<img src="https://www.target.com/?cacheBuster=1">
</body>
</html>I hosted the malicious page and tested it using a second account, but the attack failed due to the browser's strict enforcement of SameSite cookie attributes.
SameSiteis an HTTP cookie attribute that controls whether cookies are automatically sent along with cross-site requests.
Because the request originated from the attacker's domain, the browser blocked the victim's session cookies from being included in the cross-site request. As a result, the request to the homepage arrived unauthenticated. Meaning there was no active session or JWT to be stored in the cache in the first place.
Mind you, the target hadn't implemented any SameSite attributes on the cookie, but modern browsers will apply SameSite=Lax by default if no attribute is specified. This protection is insufficient and would allow cookies cross-site under certain conditions.
How I bypassed SameSite
I researched a bit and I found that, even though SameSite=Lax restrictions will block your cookies from being included in requests originating from other sites, but if the request came from a top-level navigation, then these rules don't apply.
A
Top-Level Navigationoccurs when the address bar of the browser actually changes to a new URL. A navigation where the entire viewport (the main window) transitions to a completely new website, replacing the previous document structure entirely.
Because the exploit relies on Top-Level Navigation, the browser will include the victim's authenticated cookies even under "Lax" restrictions. I identified multiple vectors to force this navigation:
- Automated Meta-Refresh: The HTML
<meta>tag can be used to trigger an automatic top-level redirect. This requires zero user interaction beyond simply loading the attacker-controlled page. - User-Driven Navigation: As a fallback, an
<a></a>tag is included. While this requires a click, it serves as a reliable secondary vector if the automated redirect unlikely fails.
My new PoC looked like this:
<!DOCTYPE html>
<html>
<head>
<title>WCD PoC</title>
<!--meta tag auto performs top-level navigation-->
<meta http-equiv="refresh" content="0; url=https://www.target.com/?cacheBuster=1"">
</head>
<body>
<h3>Redirecting for PoC…</h3>
<!--backup option of meta tags fail to auto redirect-->
<p>If you are not redirected automatically, <a href="https://www.target.com/?cacheBuster=1">click here</a>.</p>
</body>
</html><!DOCTYPE html>
<html>
<head>
<title>WCD PoC</title>
<!--meta tag auto performs top-level navigation-->
<meta http-equiv="refresh" content="0; url=https://www.target.com/?cacheBuster=1"">
</head>
<body>
<h3>Redirecting for PoC…</h3>
<!--backup option of meta tags fail to auto redirect-->
<p>If you are not redirected automatically, <a href="https://www.target.com/?cacheBuster=1">click here</a>.</p>
</body>
</html>I visited the hosted page, and it WORKED! Due to the tag, a quick top-level redirect occurs with the cookies included and the response gets cached.
The attacker makes the same GET request to the homepage with the same /?cacheBuster=1parameter and they get served the victim's cached response. From there, the attacker simply extracts the victim's JWT from the source code and injects it into their own requests to fully take over the account.
Intent Redirection is very common in Android app, read source code to understand how to exploit.
Impact and Payment
An attacker can steal the JWT of any user who visits the malicious page. This was classified as a HIGH and I got paid $2000.
Timeline of Disclosure:
March 4 — Reported
March 11 — Acknowledged
March 13 — Provided extra detailed explanations upon request
March 19 — Triaged and $2,000 Bounty paid
May 7 — Fixed and $50 Retest completed
May 8 — Closed as Resolved
One last shameless plug, IDORs are an easy $500, just swap some UUIDs.
Hi…bye
Thanks for reading this, if you have any questions, you can DM me on X @tinopreter. Connect with me on LinkedIn Clement Osei-Somuah.