October 17, 2026
Web Cache Deception: How A GraphQL Query in the Archives Led Me to Cached Profile Data
Recon sometimes reveals behavior that never appears in the application’s normal traffic.

By Adini Rodini Rodney
3 min read
While testing a target, every GraphQL request I observed used POST. Then I found something different in the web archives: a URL carrying a GraphQL query in its query string.
That archived request became the starting point for an investigation into cached user data.
The target, routes, operation names, and identifying details below have been replaced with illustrative examples.
The archived request
The URL looked roughly like this:
GET /api/query/publicContent?query=...&variables=...
Host: example.comGET /api/query/publicContent?query=...&variables=...
Host: example.comI replayed it, and the server accepted it.
The response contained public information, so there was no sensitive exposure at this stage. But it revealed something useful: although the frontend sent GraphQL requests through POST, the server also accepted queries through GET.
I wanted to see whether that behavior extended to authenticated operations.
Moving from public content to a user profile
After registering and signing into my test account, I found a GraphQL operation that returned my profile information.
A simplified version looked like this:
POST /api/query/accountProfile HTTP/1.1
Host: example.com
Content-Type: application/json
Cookie: session=REDACTED
{
"query": "query AccountProfile($userId: String!) { accountProfile(userId: $userId) { fullName email phoneNumber } }",
"variables": {
"userId": "TEST_ACCOUNT_ID"
}
}POST /api/query/accountProfile HTTP/1.1
Host: example.com
Content-Type: application/json
Cookie: session=REDACTED
{
"query": "query AccountProfile($userId: String!) { accountProfile(userId: $userId) { fullName email phoneNumber } }",
"variables": {
"userId": "TEST_ACCOUNT_ID"
}
}The actual response included additional personal information, including address and date-of-birth fields.
Access control appeared to work during my testing. Changing the identifier did not simply let me retrieve another account's profile.
Next, I moved the GraphQL query and variables into the URL and changed POST to GET.
The server still returned my authenticated profile.
That alone did not demonstrate a vulnerability. The next question was whether the response could enter a shared cache.
Adding a file extension
I appended .pdf to the operation path:
https://example.com/api/query/accountProfile.pdf?query=...&variables=...https://example.com/api/query/accountProfile.pdf?query=...&variables=...The endpoint continued returning profile data.
On the first request, I observed a cache miss. Repeating the request produced a cache hit.
The important check came afterward: requesting the same URL without my session cookie returned the cached profile response.
This suggested a mismatch between how the application handled the modified route and how the caching layer treated it. The application still processed a dynamic GraphQL request, while the response appeared eligible for shared caching.
The .pdf suffix had not turned the response into a document. It was still user-specific JSON.
The reproduction limitation
My initial report described retrieving the cached response through a private browser window. During triage, however, that browser reproduction stopped working consistently.
Further testing required a crafted command-line request containing an Origin header. Populating the cache with authenticated profile data also required a valid session cookie.
The sequence I documented was:
- Send the modified GET request with my test account's authentication cookie and the required header.
- Observe the profile response being cached.
- Request the same URL without the authentication cookie.
- Retrieve the cached profile data.
That distinction mattered.
I had demonstrated the behavior using my own authenticated session. I had not established a reliable way for an attacker to trigger it through an ordinary link click by another user.
The documented victim scenario required someone to execute a crafted command containing their session cookie. That was a substantial practical limitation.
The report outcome
The report was closed as Informative under a policy excluding issues that required unlikely user interaction.
The result was not an accepted bounty finding, but the investigation still taught me something useful: demonstrating cached personal data and demonstrating a realistic attack against another user are separate parts of a report.
Both need clear evidence.
What I took away
The strongest lead came from comparing archived behavior with the application's current traffic.
The frontend used POST. An archived URL showed that GET was also accepted. Testing that behavior against an authenticated operation then exposed an unexpected interaction with the cache.
For future investigations, I would establish the victim-triggering conditions earlier and document exactly which headers, cookies, and browser behaviors are necessary.
I would also keep the demonstrated impact focused on the profile data actually reproduced, rather than extending the claim to other sensitive operations without equivalent evidence.
A cache hit is a clue. Retrieving another session's response is stronger evidence. A realistic way to cause that response to be cached is what completes the attack story.