Back to News & Insights
JavaScript August 29, 2026 · 6 min read

How I Found an SSR Cache Isolation Failure in a React Data Fetching Library

There's a certain kind of bug that only shows up when you stop thinking about what code does and...

How I Found an SSR Cache Isolation Failure in a React Data Fetching Library

There's a certain kind of bug that only shows up when you stop thinking about what code does and start thinking about when it does it.

Most security researchers spend their time asking "can an attacker make this code do something bad?" That's a fine question. But it misses an entire category of vulnerabilities that have nothing to do with malicious input. Sometimes the code does something bad all on its own, just because of how it's structured.

I was auditing a popular React data fetching library. You've probably used it or at least seen it in a Next.js project. It's the kind of library that handles caching, revalidation, and all the other unglamorous parts of making API calls from React components.

The library has a feature that's pretty common in the React ecosystem: it works differently depending on whether you're running on the client or the server. On the client, you want caching to persist across page navigations. On the server—during server-side rendering—each request should be completely isolated from every other request. That's Security 101 for SSR: if user A and user B both trigger a server render at the same time, neither should be able to see the other's data.

So I started asking a simple question: how does this library manage its cache during server-side rendering?

I traced the cache initialization back to its source. What I found was about as simple as it gets:

That's a module-level variable. A single Map object, created once, sitting at the top of a file. Every part of the library that needs a cache references this same object.

On the client, this makes total sense. You want one cache for the entire application. When the user navigates between pages, the cache persists. That's the whole point.

But on the server, module-level variables are dangerous. In a Node.js environment, modules are loaded once and shared across all incoming requests. That means this single cache Map is shared across every user who hits the server. Concurrently.

I looked for a mechanism that would provide request-scoped isolation by default. Some kind of AsyncLocalStorage wrapper, or a factory function that creates a fresh cache per request, or even a check that said "hey, you're in SSR mode, here's an isolated cache instead."

Here's what the vulnerability looks like in practice. Imagine a Next.js application that uses this library to fetch user-specific data during server-side rendering. The component looks something like this:

The cache key is /api/user-data. It's the same for every user. The library uses this key to look up cached data before making a fetch request.

Now here's what happens when two users hit the server around the same time: User A's request starts rendering. The library checks the global cache for /api/user-data. It's empty, so it fetches the data from the API and stores it in the cache. The cache now contains User A's data. User B's request starts rendering a few milliseconds later. The library checks the global cache for /api/user-data. It finds User A's data sitting there. It uses it. No fetch needed. User B's HTML response contains User A's name, email, or whatever else was in that cached payload.

No attacker needed. No malicious input. Just two users using the application at the same time, and the library's default behavior hands one user's data to another.

The library does provide a way to isolate caches per request. There's a provider component that you can wrap your app in, and it creates a fresh cache scope. But it's opt-in. The default configuration doesn't do this.

And here's the thing about defaults: they're what most people use. If you're a developer integrating this library into your Next.js app, you'll probably follow the docs, use the default setup, and never realize that you're sharing cache state across all your users. The docs might mention the provider in passing, but unless you're specifically looking for SSR isolation concerns, you're not going to connect the dots.

This is one of those situations where the secure option exists but the insecure option is the default. And in security, defaults matter more than anything.

This isn't unique to this particular library. Module-level mutable state in server-side JavaScript is a pattern that shows up everywhere. Any time a Node.js application shares a mutable variable across request boundaries, there's potential for data leakage.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation