Safeguard
Security Guides•

What is cross-request state pollution in Vue SSR?

Cross-request state pollution leaks one user's data into another user's server-rendered page. See how a shared store causes it and the per-request fix.

Safeguard Security Team
3 min read

Cross-request state pollution is when server-side rendering reuses a shared object across requests, so data from one user leaks into another user's page. In Vue SSR it happens when a module-level store is mutated with user-specific data. The fix is a new app and store per request.

Why SSR changes the rules

In a browser-only app, every page visit loads the JavaScript modules fresh, so module-level state belongs to one visitor. In server rendering, the Vue guide explains, application modules are typically initialized once when the server boots. The same module instances serve many requests. A singleton store at module scope is therefore shared by every visitor.

If a request writes user-specific data into that shared object, say a name, an email address or a token loaded for personalization, the next request can render it. Vue calls this cross-request state pollution. It is a data exposure bug that tests with one user will never show.

A picture of the failure

Request A signs in as Ana. The server loads her profile into the shared store and renders her page. Request B arrives from a different visitor before anything resets the store. If B's render reads from the store before its own data loads, it can output Ana's details. Timing makes it intermittent, which makes it hard to notice and easy to dismiss.

The recommended fix

The guide's answer is to create a new instance of the entire application, including the router and global stores, on each request. Then provide the store at the app level and inject it in components, instead of importing a singleton directly. In code that looks like a factory function called per request.

import { createSSRApp } from 'vue'
import { createStore } from './store.js'

export function createApp() {
  const app = createSSRApp(/* ... */)
  const store = createStore(/* ... */)
  app.provide('store', store)
  return { app, store }
}

Reinitializing every module per request would also work in principle, but it costs performance, so the factory pattern is preferred. The guide notes that state management libraries like Pinia are designed with this in mind.

How to check your own app

  1. Search for state declared at module scope: top-level reactive objects, refs and store instances.
  2. Ask whether any server code path writes request-specific values into them.
  3. Look for other module-level caches, such as memoized API clients that keep an authorization header.
  4. Test with two sessions sent in quick succession and compare the HTML each receives.
  5. Log a request ID with any shared state write, and alert if one request reads a value written by another.

Related SSR notes

Vue's SSR guidance also warns that lifecycle hooks such as mounted do not run on the server, so timers set up in setup code stay alive forever. That is a memory leak rather than a leak of data, but it comes from the same root: server code lives much longer than a browser page.

Quick check

Does this only affect Vue?

The Vue guide describes it for Vue, but any server rendered framework with shared module state needs the same care.

Is a per-request store slow?

Creating a small app and store object per request is the pattern the guide recommends. Re-initializing all modules is the expensive approach it avoids.

Safeguard's free scan at gold.safeguard.sh/scan checks the dependency side of an SSR project, including the rendering and store packages.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.