Users logged in as someone else: how to find the cause and fix it
If users see another person’s account, a shared cache or a shared server variable is the most likely cause. Take the affected pages offline, purge the cache, and end all sessions. Then set Cache-Control: private, no-store on signed-in pages. Then remove user data from variables that live outside the request handler.
This guide shows how to confirm the cause, how to stop the leak, and how to check what users saw. It uses Vercel, Next.js, and Supabase as examples. The same rules apply to other hosts.
What does “users logged in as someone else” mean?
It means that one visitor receives a page or a session that belongs to another user. The visitor did not sign in as that user. The system gave the wrong data to the wrong person.
This is a serious fault. The other user’s name, email, orders, or messages can show on the screen. In some cases the visitor can also take actions as that user.
Two causes explain most cases:
- A shared cache stores a personal response. A CDN, a proxy, or a framework cache stores the page of user A. It then serves the same stored page to user B.
- A shared variable on the server holds one user. The server code keeps the current user in a variable outside the request handler. A second request changes it, or reads it, while the first request still runs.
A third cause exists on the browser side. Two people use the same browser profile, and the app stores the session in a place that both can read. This case is rare. It does not explain users on different devices.
The MDN guide to HTTP caching describes the risk. It says that if personalized content is stored in a cache other than a private cache, other users may be able to get that content. A private cache is tied to one client, such as a browser. A shared cache sits between the client and the server and can serve many users.
The Supabase server-side client documentation gives the same warning for sessions. It says that caching of HTTP responses can cause users to receive another user’s session.
How do I confirm which cause I have?
Run these checks. They take about 20 minutes. Use two test accounts, A and B, and two different browsers.
Check 1: Read the cache headers
- Sign in as user A in browser 1.
- Open the page where the problem appears. Open DevTools and the Network tab.
- Select the main document request. Read the response headers.
- Find
Cache-Control,Set-Cookie,Age, and, on Vercel,x-vercel-cache.
Use this table to read the result:
| What you see | What it means |
|---|---|
Cache-Control: s-maxage=... or public on a signed-in page |
A shared cache may store the page. |
x-vercel-cache: HIT on a signed-in page |
The CDN served a stored copy. |
Age with a value above 0 |
A shared cache served a stored copy. |
Cache-Control: private or no-store |
A shared cache must not store the page. |
The Vercel CDN cache documentation says that a response can be cached when the request has no Authorization header and the response has no set-cookie header. The response must also have no private, no-cache, or no-store directive. It also says that the CDN does not cache a response that has Vary: Cookie.
This has a consequence for sessions. Your app may read the session cookie on the server, and it may not set a cookie in the response. If the response also has s-maxage, the CDN can store it. The cache key does not include the cookie. The CDN then serves the stored page to every visitor of that address.
Check 2: Request the page twice with different users
Use a terminal. Replace the cookie values with the session cookies of your test users.
curl -s -D - -o /dev/null https://your-app.example/dashboard \
-H "Cookie: YOUR_SESSION_COOKIE_FOR_USER_A"
curl -s https://your-app.example/dashboard \
-H "Cookie: YOUR_SESSION_COOKIE_FOR_USER_B" | grep -i "user-a-email@example.com"
The first command prints the response headers for user A. The second command requests the same page as user B and searches for the email of user A. If the search finds the email of user A, the page is leaking. Run the pair a few times. A cache can need one request to store the page.
Check 3: Look for shared variables in the server code
Search your server code for variables that hold user data outside a request handler. Use these searches:
grep -rnE "^(let|var) .*(user|session|token)" app/ lib/ src/ --include=*.ts --include=*.tsx --include=*.js
grep -rnE "^(const|let) .*= *(createClient|createServerClient)\(" app/ lib/ src/
The first search finds top-level let and var variables with names that suggest a user. The second finds clients that someone created once, at the top of a file, and not in each request. A server client that reads cookies must be new for each request.
Check 4: Check the framework cache
Some frameworks render a page once and store it. A page that reads cookies or headers must run on each request. Check the rendering mode for each signed-in route in your framework documentation. In a build log, a static page is often marked with a symbol or a label. A signed-in page must not have that label.
How do I stop the leak now?
Do these steps in order. Steps 1 to 3 stop new leaks. The other steps remove the cause.
Warning: Ending all sessions logs out every user. This is correct after a leak, because you cannot tell which sessions were exposed. Tell your users before you do it, if you can.
- Take the affected pages offline. Show a maintenance page for the signed-in routes. If you cannot do this, remove the
s-maxageheader and redeploy. - Purge the CDN cache. Clear the stored copies of every signed-in route. In Vercel, use the cache purge function of the project. If you use another CDN in front of your host, purge that cache too. A fix in your code does not remove copies that are already stored.
- End all sessions. Make every user sign in again. The method depends on your auth service. If your app stores sessions in a table, delete the rows. If you use signed tokens, rotate the signing secret. Check your auth documentation for the exact step, because it differs by service.
- Set private cache headers on signed-in responses. See the code below.
- Remove user data from shared variables. See the section on server code below.
- Create the auth client in each request. The Supabase documentation says that the server client needs the cookies of the current request. Create it inside the handler.
- Redeploy and repeat the tests. Run Check 1 and Check 2 on the new deployment.
- Purge the cache once more after the deploy. A copy that was stored before the fix can still exist.
How do I set the correct cache headers?
For a page or a route that depends on the user, send Cache-Control: private, no-store. MDN lists no-cache, private for personalized content that is not sensitive. It lists no-store for highly sensitive data. Use no-store when you are not sure. A shared cache must then not keep the response.
For a Next.js project, set the header for the signed-in paths in next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: "/dashboard/:path*",
headers: [{ key: "Cache-Control", value: "private, no-store" }],
},
{
source: "/api/me/:path*",
headers: [{ key: "Cache-Control", value: "private, no-store" }],
},
];
},
};
module.exports = nextConfig;
For a route handler, set the header in the response:
export async function GET() {
const data = await loadDataForCurrentUser();
return Response.json(data, {
headers: { "Cache-Control": "private, no-store" },
});
}
Note: The Vercel documentation says that a header that a function returns overrides the header that you set in the configuration file. Set the header in one place, and test the final response.
Do not use public on a signed-in response. MDN says that public makes a response storable even when the request has an Authorization header. Do not use Vary: Cookie as a fix. MDN says to use Cache-Control: private for cookie-based content.
How do I remove shared state from server code?
A server process handles many requests. A variable outside the request handler exists once for the whole process. Every request that the process handles can read and write it.
This code is wrong:
let currentUser: User | null = null;
export async function GET(req: Request) {
currentUser = await getUserFromCookie(req);
const orders = await loadOrders(currentUser.id);
return Response.json(orders);
}
When two requests run at the same time, they share currentUser. The first request can read the user of the second request after an await.
This code is correct:
export async function GET(req: Request) {
const user = await getUserFromCookie(req);
if (!user) return new Response("Unauthorized", { status: 401 });
const orders = await loadOrders(user.id);
return Response.json(orders, {
headers: { "Cache-Control": "private, no-store" },
});
}
The user lives inside the handler. Each request has its own copy. Apply the same rule to auth clients, to tokens, and to memoized functions. A function that caches its result must include the user id in the cache key. If you use a cache in memory, store each value under a key that has the user id.
Also check that each database query filters by the current user. Row level security protects against a wrong query. See BOLA in AI-generated CRUD for the related fault where an id in the URL returns data of another user.
How do I check what users saw?
You need to know which pages leaked, for how long, and to whom. Use the data that you have.
- Find the time window. Look at your deploy history. Find the first deployment that had the faulty header or the shared variable. The leak started then.
- List the affected routes. Make a list of all routes that returned personal data and had a shared cache header.
- Read the host logs. In the logs of your host, search for requests to those routes. On Vercel, look at the cache status of each request in the runtime logs, and at the
x-vercel-cachevalue if you log it. AHITon a signed-in route is a leak. - Match requests to users. If you log a user id, find requests where the user id of the request does not match the data in the response. If you do not log the user id, you can only know the routes and the times.
- Check actions as well as views. A shared session can also let a visitor change data. Look in your database for rows that changed in the window and that the owner says they did not change. Check emails, passwords, and payment settings first.
- Check the most exposed data. List what each leaked page contained. A page with a name is a smaller problem than a page with an address, an invoice, or a message.
Write down what you find. Include the dates, the routes, the data types, and the number of users that you think were affected.
If personal data went to the wrong person, tell the affected users. Some laws require a notice within a set time. Ask a lawyer which rules apply to you. Do this early, because the time limit can start when you learn about the leak.
How do I stop it from happening again?
Add controls that find the fault before users do.
- Add a two-user test. Write a test that signs in as user A and user B, requests each signed-in route, and checks that the response of B never contains data of A. Run it after each deployment.
- Add a header test. Request each signed-in route with a session and check that
Cache-Controlhasprivateorno-store. Make the build fail if it does not. - Set a default. Add a middleware that sets
Cache-Control: private, no-storeon every route that reads a session cookie. A new route is then safe without a person remembering. - Ban top-level user state. Add a lint rule or a review rule. A server file must not have a
letvariable that holds a user, a session, or a token. - Read AI-written caching code. Coding tools often add
s-maxage,revalidate, or memoization to improve speed. Read each change that touches a route with a session. - Keep static and personal pages apart. Put public pages on cached routes. Put personal pages on routes that the CDN never stores. Use one clear path prefix for each group.
- Log the cache status. Log
x-vercel-cacheor the equivalent, and the user id, for signed-in routes. The log then shows a leak quickly.
The Vercel security guide lists more settings for the host. The authentication implementation guide covers the session rules for the login flow.
When should I ask for help?
Ask a security professional for help in these cases:
- Visitors could change data, or spend money, as another user.
- You cannot find the cause with the checks above.
- The leak includes health, payment, or identity data.
- You have no logs that show which users saw which pages.
- You must send a notice to users or to a regulator.
Save the headers, the logs, the deploy history, and the test results. The record shows the window of the leak and the data that it reached.
Common questions
Is this a bug in my login library?
Does Vary: Cookie fix a CDN that serves the wrong page?
Can this happen without a CDN?
Should I tell my users?
Will clearing the browser cookies fix it for the user?
Check for cached sessions
A VibeEval scan requests the signed-in pages of your deployed app and reports cache headers that let a shared cache store personal pages.
14-day free trial · No credit card · Cancel anytime