Some time ago the website you are reading was written in PHP, it had an XSS vulnerability that I wrote about previously and was an interesting realization of how anyone can make mistakes. Eventually, I rewrote it to use NextJS and an Axum backend, which allowed me to make use of many more features targeted at static websites such as pre-fetching and caching.
While being less afraid of XSS vulnerabilities, the new architecture also has new problems. Specifically, while hearing about an example of Cache Deception, it sounded eerily similar to my website's setup. After testing it, sure enough, it was vulnerable. This would have allowed anyone to send me a link while I'm logged in, and if timed correctly, receive all hidden posts and their contents unauthenticated from the cache!
The Technique
The idea of "Cache Deception" is that if you can somehow make another user cache their authenticated responses to a predictable URL, an attacker without the right authorization may request that same URL and receive the cached response from the victim, enabling them the read the content.
Imagine a website that:
- Caches all URLs starting with
/static
, which makes sense because they should not change often - Has a
/profile
endpoint where a signed-in user can receive personal information about their profile
Using ../
(path traversal) sequences in a URL, it is possible for an attacker to construct one that starts with /static
but the server will handle it as /profile
:
While this may work in your Burp Suite proxy, Cache Deception is a client-side attack meaning you need to trigger this behavior from the victim's browser. If from an attacker's site, we would make the visitor request /static/../profile
, the ../
will be resolved by the browser before being sent to the server, so it won't be cached. Therefore we need to make a URL that isn't recognized as a path traversal by the browser but is by the backend server. Such as:
The browser will see ..%2Fprofile
as one big part of the path, but the backend server may decode %2f
-> /
, and then recognize the created path traversal sequence to create /profile
. If the caching server also doesn't do this, it will see the requested URL starts with /static
and cache it!
So, you'd send the victim to a URL like the following:
https://example.com/static/..%2Fprofile
One popular example was that ChatGPT was vulnerable. What triggered me was a small tip by Justin Gardner while listening to the Critical Thinking Bug Bounty Podcast. He mentions that "when there isn't a separate API host, just a /api
, [...] you're serving static assets and APIs on the same host". This is a good indicator that the site may be vulnerable to cache deception.
My Configuration
I also have authenticated endpoints under /api
. Together with aggressive caching of basically all content because a blog website should just be static. In Cloudflare, I made some Cache Rules to allow everything by default, only bypass the cache for a few authenticated paths:
After Cloudflare, the request goes to my Nginx server which routes the request to the correct server. Importantly, Nginx decodes the URI before passing it to the proxy_pass
host. For matching the location ...
directive, it also decodes and resolves path traversal sequences before matching the path.
Finally, the Axum backend expects paths like /blog/hidden
with a valid Cookie:
to return content from hidden posts.
Exploit
With this knowledge, we can construct the following URL which Cloudflare will not decode, and see that it starts with /static
, a directory that may be cached.
https://jorianwoltjer.com/static/..%2Fapi%2Fblog%2Fhidden
When Nginx reads the URI, it will decode it to /static/../api/blog/hidden
and resolve the ../
, concluding that /api/blog/hidden
starts with /api
. It routes this to the API server which receives the decoded version /static/../api/blog/hidden
, Axum resolves this path traversal as well and returns the response containing hidden blog posts.
To initiate the attack, the victim (me) must be logged-in, and visit the URL above. This will trigger the following request:
Notice the Cf-Cache-Status
is not BYPASS
, so it will be cached by Cloudflare. The attacker can now request the same URL without cookies to receive this response:
In the end, I fixed the issue by simply not relying on paths, but instead bypassing the cache if the session=
cookie is set. That's not set for regular visitors, so they can get the fastest possible responses from Cloudflare. But when I am logged in as an administrator the cookie is set and no content returned should be cached for others to snoop up.
Conclusion
This was a pretty high-impact vulnerability that I luckily found myself, but I am certain that more configurations are vulnerable to this. Go and check for Cache Deception on your next target, keeping in mind other possible cache rules such as file extensions to get authenticated responses cached for you to retrieve!
After discovering this I did a deep dive to learn all the known tricks of caching on the server-side and even client-side. There's a lot to explore in this area which I summarized it in my Gitbook: 🌐 Web / Client-Side / Caching.