HomeGuidesPrivate data in page source: __NEXT_DATA__ and RSC leaks
Guide
Private data in page source: __NEXT_DATA__ and RSC leaks
Find user data your Next.js app ships in page source: __NEXT_DATA__, the RSC payload, Server Action returns, static pages and CDN caching, with tested fixes.
Independent notes from VibeLocked. Not a pentest, and not affiliated with the tools named below.
A page can look locked and still hand over the data. The login screen renders, the dashboard is hidden, and the server already sent the user record, the order list, or the admin stats inside the HTML. It does not show on screen. It sits in a script tag that anyone can read with View Page Source or one curl request.
In Next.js this happens through four doors: the __NEXT_DATA__ script in the Pages Router, the React Server Components (RSC) payload in the App Router, the return value of a Server Action, and caches that store one person's page and serve it to the next. Remix, SvelteKit and Nuxt have the same doors with different names. AI coding tools open all of them, because passing the whole object is the shortest code that renders.
This guide shows how to check your own app while signed out, what each leak looks like, and the fixes that held up in our tests. If your problem is an admin page that only hides itself in the browser, start with client-side auth is not security. If it is API keys in the JavaScript bundle, see keep secrets out of frontend code. This page is about data: rows, profiles and records that leave the server inside the page.
Checked and tested on October 22, 2026 against the Next.js, React, Vercel, Cloudflare and OWASP docs linked below. We built a small Next.js 16.4.2 app (React 19.3.1, App Router and Pages Router side by side) with fake users, reproduced every leak on this page with curl, then applied each fix: 42 checks, all passing. VibeLocked is independent and not affiliated with Vercel, Meta or any framework named here.
The short version
- Hidden is not protected. If the server sent it, the visitor has it. A field your component does not render is still in the payload.
- Check signed out first. Private window, View Page Source, search for
__NEXT_DATA__,self.__next_fand field names likeemail,password,token,roleandstripe. - Return DTOs, not rows. Select only the columns you need and map them to a small object before they reach a Client Component,
props, or a Server Action's return. - Check auth before you fetch. Read the session, redirect or 404, then query. Never fetch first and let the client decide what to show.
- Keep personal pages dynamic and private. No
generateStaticParamsorgetStaticPropsfor per-user pages, and nopublicors-maxageon a response that depends on the cookie. - Add guard rails.
import 'server-only'on data modules, and React's taint APIs if you accept their experimental status. Neither replaces step 3.
Why it happens
Server rendering produces two things: the HTML you see, and the data the browser needs to make that HTML interactive (hydration). The data is serialized into the page. Next.js's getServerSideProps docs say it directly: props "can be viewed on the client as part of the initial HTML", so "make sure that you don't pass any sensitive information". The Next.js data security guide gives the App Router version: passing a full userData row to a Client Component "exposes all the fields in userData to the client".
OWASP files this as API3:2023 Broken Object Property Level Authorization: the user may see the object, but not every property on it. Its advice is to "cherry-pick specific object properties" instead of serializing whole objects. The OWASP Next.js Security Cheat Sheet applies the same rule to Client Component props, page props and Server Action returns. The bug is CWE-200, exposure of sensitive information to an unauthorized actor, and when a cache is involved, CWE-524.
A real case: in October 2026, a franchise's internal dashboard showed a login screen while the page source already held store contacts and financial data. We covered it in the admin page guide.
Check your own app, signed out
Only test apps you own. Nothing here asks you to paste a key, a cookie or a token anywhere, or to run code in the browser console.
1. View the source in a private window. Open a private or incognito window so you are signed out. Visit your dashboard, settings, admin, billing and any /[id] pages directly. Press Ctrl+U (Cmd+Option+U on a Mac) for View Page Source, then search (Ctrl+F) for:
__NEXT_DATA__(Pages Router props)self.__next_f(App Router RSC payload)- field names:
email,password,hash,token,secret,role,stripe,customer,phone,address,notes,service_role
Search for field names, not full values. In the RSC payload the JSON sits inside JavaScript strings, so it shows up as \"email\":\"...\", and values that start with $ are escaped to $$. In our test a bcrypt hash starting $2b$10$ appeared as $$2b$10$, so a search for the exact hash would have missed it.
2. Fetch it with curl. curl sends no cookies, so it sees exactly what a stranger sees:
curl -s https://your-app.example/dashboard -o page.html
grep -c '__NEXT_DATA__' page.html
grep -c 'self.__next_f' page.html
grep -o -i -E 'password_?hash|reset_?token|stripe_?customer|service_role|api_?key|role|email' page.html | sort | uniq -c
Counts for __NEXT_DATA__ or self.__next_f are normal on any Next.js page. The field names are what matter.
3. Repeat signed in as a normal user. Sign in with a test account in your normal window, open the same pages, and use View Page Source again. You are looking for fields the user should not have: their own password hash or reset token, internal notes, other people's records, admin flags. A user seeing their own email is fine. A user seeing everyone's is not.
4. Check the Pages Router data route. Pages with getServerSideProps also answer at /_next/data/<buildId>/<page>.json. Find the build id in the page source ("buildId":"...") and fetch that URL with curl. In our test it returned the same full props as __NEXT_DATA__.
5. Watch Server Actions in DevTools. Open DevTools, go to the Network tab, filter to Fetch/XHR, and click the buttons that call actions. The responses with type text/x-component are action results. Read them for fields the UI does not show.
6. Check cache headers. For each personal page:
curl -s -D - -o /dev/null https://your-app.example/dashboard | grep -i -E '^(cache-control|age|x-vercel-cache|cf-cache-status|x-nextjs-cache):'
A personal page should say private or no-store. public, s-maxage, an age header, or a cache status of HIT or PRERENDER on a page that depends on who is signed in is a red flag.
7. Read your build output. next build prints a route table. Routes marked ○ (static) or ● (SSG) were rendered once at build time and are the same for everyone. Any user-specific route with those markers is a bug. Routes that read cookies show ƒ (dynamic).
LeakCheck also reads your deployed site the way a signed-out stranger does and flags secrets in shipped JavaScript and open Supabase or Firebase data. It does not sign in, so steps 3 and 5 are still on you.
Leak 1: a whole row passed to a Client Component (App Router)
This is the most common one. A Server Component loads the user and hands the row to a Client Component that renders one field:
// app/dashboard/page.tsx (leaks)
import { getSessionUserRow } from '@/lib/session';
import { ProfileCard } from '@/components/ProfileCard'; // 'use client'
export default async function Page() {
const user = await getSessionUserRow(); // SELECT * FROM users ...
if (!user) return <p>Signed out</p>;
return <ProfileCard user={user} />;
}
The screen says "Hello, Alice Example". The HTML also carries this, inside a self.__next_f.push(...) script:
{\"user\":{\"id\":1,\"name\":\"Alice Example\",\"email\":\"alice@example.test\",
\"passwordHash\":\"$$2b$10$...\",\"stripeCustomerId\":\"cus_...\",\"resetToken\":\"rst_...\",...}}
In our test the password hash, Stripe customer id, reset token and internal notes were all in the HTML, none of them in the visible text. A client side navigation (an RSC: 1 request) returned the same row again. Server Components themselves stay on the server, as the Server and Client Components docs explain, but every prop that crosses into a 'use client' file is serialized.
Fix: a DTO from a server-only data layer. The Next.js guide calls this a Data Access Layer: it only runs on the server, checks authorization, and returns minimal Data Transfer Objects.
// lib/dal.ts
import 'server-only';
import { getSessionUserRow } from './session';
export async function getProfileDTO() {
const row = await getSessionUserRow();
return row ? { name: row.name } : null;
}
// app/dashboard/page.tsx (fixed)
import { getProfileDTO } from '@/lib/dal';
import { ProfileCard } from '@/components/ProfileCard';
export default async function Page() {
const profile = await getProfileDTO(); // { name } only
if (!profile) return <p>Signed out</p>;
return <ProfileCard user={profile} />;
}
Type the Client Component's props narrowly too ({ user: { name: string } }, not { user: User }). The Next.js guide calls a broad props type "a bad props interface" because it invites passing everything.
Better still, do not load the columns at all: supabase.from('profiles').select('id, display_name') instead of select('*') (Supabase select), Prisma's select or omit (which can also be set globally on the client for fields like password), or Drizzle's partial select. With Supabase called from the browser, also lock the columns in the database, as in column level security for plan and credits.
Leak 2: getServerSideProps returning the row (Pages Router)
// pages/account.tsx (leaks)
export const getServerSideProps = async ({ req }) => {
const id = userIdForToken(req.cookies.session);
return { props: { user: id ? await selectUserById(id) : null } };
};
export default function Page({ user }) {
return <p>{user ? `Hello, ${user.name}` : 'Signed out'}</p>;
}
The whole row lands in <script id="__NEXT_DATA__" type="application/json"> as plain JSON, and /_next/data/<buildId>/account.json returns it again. Both passed in our test. The fix is the same: return { profile: { name: row.name } }. Treat getStaticProps and getInitialProps the same way, since their props are serialized too.
Leak 3: fetch first, hide later
AI tools often write the data query at the top of the page and the permission check in the UI:
// app/admin/orders/page.tsx (leaks)
export default async function Page() {
const orders = await selectAllOrders(); // runs for everyone
const user = await getSessionUserRow();
return <AdminPanel orders={orders} isAdmin={user?.role === 'admin'} />;
}
// AdminPanel ('use client'): if (!isAdmin) return <p>Please sign in as an admin.</p>;
Signed out, our test page said "Please sign in as an admin." and its source held every customer's shipping address. The props were serialized before the Client Component decided to hide them.
Fix: authorize, then query.
// app/admin/orders/page.tsx (fixed)
import { redirect } from 'next/navigation';
export default async function Page() {
const user = await getSessionUserRow();
if (user?.role !== 'admin') redirect('/login');
const orders = (await selectAllOrders()).map(({ id, item, total }) => ({ id, item, total }));
return <AdminPanel orders={orders} />;
}
Signed out, that page returned a 307 to /login with no data. Put the check in the data layer as well, so a second page that reuses selectAllOrders cannot forget it. For roles that come from data the user cannot edit, see the admin page guide. For one user reading another user's record by id, see IDOR in Next.js and Supabase.
Leak 4: Server Actions that return too much
A Server Action's return value goes back to the browser, and the action is a public POST endpoint. The Next.js guide says to "only return what the UI needs, not raw database records", and React's 'use server' docs say to treat arguments as untrusted input.
'use server';
// leaks: no auth check, returns the whole row for any id
export async function getUserAction(id: number) {
return selectUserById(id);
}
In our test, a signed-out curl POST with the action's id and [2] as the body returned Bob's full row, password hash and reset token included. The action id was sitting in a public JavaScript chunk of the page that used it. Next.js encrypts and rotates action ids and drops unused actions from the bundle, but any action your UI calls is reachable.
'use server';
// fixed: identity from the session, DTO out
export async function getMyProfileAction() {
return getProfileDTO(); // null when signed out, { name } otherwise
}
Signed out it returned null. As Bob it returned {"name":"Bob Example"}. Our Server Actions guide covers auth, ownership and input validation for the other direction. Results from update and insert calls are a frequent source: Prisma's update returns the full updated row unless you select fields.
Leak 5: per-user pages baked at build time
generateStaticParams tells Next.js to render pages at build time instead of per request. An AI tool asked to make order pages fast may write:
// app/orders/[id]/page.tsx (leaks)
export async function generateStaticParams() {
return (await selectAllOrders()).map((o) => ({ id: String(o.id) }));
}
export default async function Page({ params }) {
const { id } = await params;
const order = (await selectAllOrders()).find((o) => String(o.id) === id);
return <p>Order {order.id}: {order.item}, ship to {order.shipTo}</p>;
}
Every order became a public HTML file. Signed out, /orders/1 returned 200 with Alice's address, x-nextjs-cache: HIT and Cache-Control: s-maxage=31536000, and the address was in a file in the build output. There is no session at build time, so there is nothing to check. The Pages Router version is getStaticProps with getStaticPaths, and Incremental Static Regeneration (ISR) has the same problem with a delay.
Fix: make the page per request and check the owner. Reading cookies() opts a route into dynamic rendering, so a session check does both jobs:
// app/my-orders/[id]/page.tsx (fixed)
import { notFound, redirect } from 'next/navigation';
export default async function Page({ params }) {
const user = await getSessionUserRow(); // reads cookies()
if (!user) redirect('/login');
const { id } = await params;
const order = (await selectAllOrders()).find((o) => String(o.id) === id && o.userId === user.id);
if (!order) notFound();
return <p>Order {order.id}: {order.item}, ship to {order.shipTo}</p>;
}
Signed out it redirected, as Bob it returned 404, as Alice it returned her order with Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate, and no prerendered file existed. If a personal page has no cookie read to anchor it, connection() also forces request time rendering. If you use Cache Components or 'use cache', keep the session read outside the cached function and pass the user id in as an argument, so it becomes part of the cache key.
Leak 6: a CDN serving one user's page to the next
Next.js already marks App Router pages that read cookies, and getServerSideProps pages, as private, no-cache, no-store. Both were true in our test. The leak comes from overriding that. The getServerSideProps docs show public, s-maxage=10, stale-while-revalidate=59 as a caching example, and AI tools copy it onto account pages:
// leaks on any shared cache
res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
Vercel's CDN cache docs say a response with s-maxage is cacheable unless the request carries an Authorization header, the response sets a cookie, or Cache-Control says private, no-cache or no-store. A cookie on the request is not on that list. On Cloudflare, HTML is not cached by default, but a "cache everything" rule plus a public header will cache it.
To show the effect without touching a real CDN, we put a small shared cache in front of our test app that caches by URL whenever the headers allow it, like a CDN rule would. Alice loaded the page first. Then Bob, and a signed-out visitor, both got Alice's email from the cache. The same page with private, no-store was never cached, and Bob got his own.
Fix: for anything that depends on the session, send Cache-Control: private, no-store (Vercel's Cache-Control guide recommends private for per-user responses and no-store for sensitive data). private keeps shared caches out under RFC 9111 and MDN. Cache public pages, not personal ones. Do not add Vary: Cookie as a fix on Vercel: the docs say a Vary on a high cardinality header like Cookie makes the response uncacheable there, so it is just a slower way to say private.
Leak 7: a server variable shared by every visitor
Not a serialization bug, but it puts one user's data in another user's page, and AI tools write it to "avoid refetching":
// leaks: module scope lives for the whole server process
let cachedUser = null;
export default async function Page() {
if (!cachedUser) cachedUser = await getSessionUserRow();
return <p>{cachedUser?.email ?? 'Signed out'}</p>;
}
In our test Alice loaded the page, then Bob and a signed-out visitor both saw alice@example.test. Use React's cache() for per-request memoizing instead. React invalidates it "for each server request", and with it Bob saw his own email. The same goes for module-level Maps keyed by nothing, and for global singletons that hold the "current user".
Guard rails: server-only and the taint APIs
import 'server-only' at the top of your data layer makes the build fail if a Client Component imports it. In our test the build stopped with "'server-only' cannot be imported from a Client Component module". It protects code and env vars from being bundled for the browser, as described under environment poisoning in the Server and Client Components docs. It does nothing about data that server code passes or returns, which is every leak above. Our frontend secrets guide covers the env var side.
React taint APIs mark objects and values that must not cross into a Client Component:
// lib/dal.ts, needs experimental: { taint: true } in next.config
import 'server-only';
import { experimental_taintObjectReference, experimental_taintUniqueValue } from 'react';
export async function getUserRowTainted() {
const row = await getSessionUserRow();
if (!row) return null;
experimental_taintObjectReference('Do not pass the whole user row to the client. Map it to a DTO.', row);
experimental_taintUniqueValue('Do not pass the reset token to the client.', row, row.resetToken);
return row;
}
Their status, as of October 2026: React's docs for taintObjectReference and taintUniqueValue say they are "not available in a stable version of React yet". Next.js exposes them behind experimental.taint, which the docs say "also enables the React experimental channel for app directory". They only work in Server Components, so they do nothing in the Pages Router.
What our test showed on Next.js 16.4.2:
- Passing the tainted row to a Client Component failed the render with a 500 and our message in the server log. Nothing leaked.
- Passing the raw reset token failed the same way.
- Returning the tainted row from a Server Action sent an error instead of the data.
- But
{ ...user, resetToken: undefined }is a new object, not tainted, and leaked the password hash. - And
`/reset?t=${user.resetToken}`is a new string, not tainted, and leaked the token.
Both gaps are documented: copies and derived values are not tracked. Use taint as a tripwire for refactors, after DTOs, not instead of them.
Remix, React Router, SvelteKit and Nuxt
The same rule holds outside Next.js: whatever the server load step returns is shipped to the browser.
- Remix and React Router framework mode. The Remix loader docs say "whatever you return from your loader will be exposed to the client, even if the component doesn't render it", and to treat loaders "with the same care as public API endpoints". React Router serializes loader data the same way.
- SvelteKit. Data returned from a server
loadis serialized and sent to the browser for hydration, and prerendered pages runloadat build time. SvelteKit's server-only modules ($lib/server,*.server.ts) block importing server code into browser code, which, likeserver-only, protects code, not returned data. - Nuxt.
useFetchanduseAsyncDataput server results in the payload (__NUXT_DATA__in the HTML). The data fetching docs recommend thepickoption to trim it, anduseFetchhas aserialize: falseoption (Nuxt 4.6 and later) to keep a result out of the payload. Trimming the API response on the server is still the real fix.
Common AI-generated mistakes
Most of these are reproduced in our test app:
select('*')orfindUniquewith noselect, then the whole row passed to a component.- Client Component props typed as the full
Useror database type, which invites passing everything. - Fetching data at the top of the page and checking the role in the Client Component (
if (!isAdmin) return <Login />). - Server Actions that return the result of
updateorinsertas is, including hash, tokens and internal columns. - Server Actions that take a
userIdoridargument and skip the session check, so a signed-out POST reads any row. getServerSidePropsreturningsession,useror a full API response to render one field.generateStaticParamsorgetStaticPathsover private records "for performance".- Copying
public, s-maxagefrom a docs caching example onto account or dashboard pages. - A module-level variable or
Mapused as a cache for the current user. - Passing
process.envor a config object to a Client Component to read one value. - Trusting
import 'server-only'or taint to stop data leaks, then spreading the row into a new object. - Calling the page "protected" because middleware or a client redirect hides it, while the HTML still carries the data.
Test it yourself
- Signed out, in a private window, open every private page and API route you know. View Page Source and search for the field names listed above. Nothing private should appear.
- Fetch the same pages with
curland run the grep from the check section. For Pages Router apps, fetch/_next/data/<buildId>/<page>.jsontoo. - Signed in as a normal test user, view the source of the dashboard and settings pages. Look for hashes, tokens, notes, other users' records and admin fields.
- In DevTools, Network tab, filter to Fetch/XHR, click every button that calls a Server Action, and read the
text/x-componentresponses. - Run
next buildand confirm every user-specific route is markedƒ. - Check
Cache-Controlon each personal page. It should sayprivateorno-store. Load it as user A, then as user B in another browser, and confirm B never sees A's data. - Rerun LeakCheck on the deployed URL after each fix.
Use test accounts with fake data. Never paste a real user's data, a session cookie or a key into a chat or a website that offers to check it.
Checklist
- Signed-out page source of every private page shows no user data, in
__NEXT_DATA__,self.__next_for anywhere else - Signed-in page source shows only fields that user is allowed to see
- Queries select only needed columns, and a server-only data layer returns DTOs
- Client Component props are typed with small, explicit shapes
-
getServerSideProps,getStaticPropsand loaders return only displayed fields - Every page and Server Action checks the session before fetching, and checks ownership
- Server Actions return
{ ok: true }or a DTO, never a raw row - No
generateStaticParams,getStaticPropsor ISR on per-user pages, andnext buildshows them asƒ - Personal responses send
Cache-Control: private, no-store, and no CDN rule caches them - No module-level variables hold per-user data; per-request memoizing uses React
cache() -
import 'server-only'on data and secret-using modules - Optional: taint APIs on user rows and tokens, with their experimental status understood
Copy-paste prompt for your AI coding tool
Paste this into Cursor, Claude Code, Lovable or Bolt. Share code only, never real user data, cookies or keys.
Audit this app for private data that reaches the browser. Do not print any secret or real user data you find; report the file, line and field names only. 1) List every Client Component ('use client') that receives props from a Server Component, and flag any prop that is a database row, an ORM result, a session object, process.env, or typed as a full User or table type. 2) List every getServerSideProps, getStaticProps, getInitialProps, Remix or React Router loader, SvelteKit load and Nuxt useFetch or useAsyncData, and flag any that return more fields than the page renders. 3) List every 'use server' function and flag any that returns a raw row or the result of insert, update or upsert, or that takes a user id or record id without reading the session and checking ownership. 4) Flag pages that fetch data before checking the session or role, or that pass data to a component that hides it with a client-side check. 5) Flag generateStaticParams, getStaticPaths, getStaticProps, revalidate or 'use cache' on routes that show per-user or private data. 6) Flag any Cache-Control header with public or s-maxage on a response that depends on cookies or the signed-in user, and any CDN rule that caches HTML for those routes. 7) Flag module-level variables or Maps that store per-user data. 8) Flag select('*') and queries without select on tables with private columns (password, tokens, notes, billing ids, roles). For each finding, propose a fix: column selection plus a DTO in a data access layer that imports 'server-only', an auth and ownership check before the query, dynamic rendering, Cache-Control private, no-store, or React cache() for per-request memoizing. Show me the diff before changing anything, and do not remove any existing auth checks.
Read the diff before you accept it, then repeat the signed-out and second-user tests above. For a wider review, see the security review prompt for AI coding tools.
How we tested
Our test app ran Next.js 16.4.2 with React 19.3.1 under Node 22, built with next build and served with next start, with fake users whose private fields contained a marker string. A second project confirmed the server-only build error. The 42 checks covered: the whole-row leak in the HTML and the RSC navigation response, the $$ escaping, the DTO fix and default headers, both taint APIs including their copy and derived value gaps and a tainted Server Action return, the fetch-first admin page and its auth-first fix, a signed-out Server Action call and the action id in public JavaScript, build-time order pages and the per-request fix (redirect, 404 for another user, private, no-store), the module-level cache and React cache(), __NEXT_DATA__ and /_next/data in the Pages Router, and the shared cache simulation for public, s-maxage versus private, no-store. We did not test a live CDN; the CDN rules above come from the Vercel and Cloudflare docs.
Related guides
- Client-side auth is not security: protect your admin page
- Next.js Server Actions security
- IDOR in Next.js and Supabase
- Keep secrets out of frontend code, for API keys in the bundle
- Exposed .env, .git and source maps on a deployed app
- Supabase column level security for role, plan and credits
- getSession vs getUser vs getClaims in Supabase, for trusting the session you check
- Ship checklist for a vibe-coded app
Sources
- Next.js: How to think about data security (DAL, DTOs, taint, Server Action return values, action ids)
- Next.js: taint config (experimental.taint, experimental React channel, caveats)
- Next.js: Server and Client Components (server-only, environment poisoning)
- Next.js: getServerSideProps (props visible in the initial HTML, caching example)
- Next.js: getStaticProps
- Next.js: generateStaticParams (build time rendering)
- Next.js: cookies (opts a route into dynamic rendering)
- Next.js: connection()
- Next.js: use server directive
- Next.js blog: How to think about security in Next.js
- Next.js v16.4.2 release
- React: experimental_taintObjectReference
- React: experimental_taintUniqueValue
- React: cache (invalidated for each server request)
- React: 'use server' (arguments are untrusted, serializable return values)
- OWASP Next.js Security Cheat Sheet (control data crossing into the browser)
- OWASP API3:2023 Broken Object Property Level Authorization
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
- CWE-524: Use of Cache Containing Sensitive Information
- RFC 9111: HTTP Caching
- MDN: Cache-Control
- Vercel: CDN cache (cacheable response criteria)
- Vercel: Cache-Control headers (private for per-user responses)
- Cloudflare: Default cache behavior (HTML not cached by default)
- Supabase: select
- Prisma: select fields
- Prisma: excluding fields with omit
- Drizzle: select (partial select)
- Remix: loader (whatever you return is exposed to the client)
- React Router: data loading
- SvelteKit: loading data
- SvelteKit: server-only modules
- Nuxt: data fetching (payload, pick)
- Nuxt: useFetch (pick, serialize)
Questions
Is data in __NEXT_DATA__ visible to users?
Yes. In the Pages Router, everything getServerSideProps or getStaticProps returns in props is written into a script tag with the id __NEXT_DATA__ in the HTML, and the same props come back as JSON from /_next/data/ when users navigate with next/link. Rendering only user.name does not hide the other fields. Return only the fields the page shows.
Can a Next.js Server Component leak data to the client?
Its own code and variables stay on the server, but every prop it passes to a Client Component is serialized into the RSC payload, which is inlined in the HTML as self.__next_f.push(...) script tags and sent again on client navigation. Pass a whole database row and every column ships, even if the component only renders one. Map rows to small objects with only the fields the client needs.
Do React taint APIs stop data leaks in Next.js?
They help, but they are a safety net. experimental_taintObjectReference and experimental_taintUniqueValue still carry the experimental prefix, need experimental.taint: true in next.config, and switch the App Router to React's experimental channel. In our test they blocked a tainted row and a raw token, including a row returned from a Server Action. A spread copy of the row and a URL built from the token still leaked. Use DTOs first.
How do I check what my site sends before login?
Open a private window, visit each private URL, and use View Page Source. Search for __NEXT_DATA__, self.__next_f and field names such as email, password, token, role and stripe. Or fetch the page with curl, which sends no cookies. Anything private in a signed-out response is a leak, whether or not it shows on screen.
Should personalized pages use Cache-Control private or no-store?
Use private, no-store for pages that show account data. private stops CDNs and other shared caches from storing the response, and no-store also keeps it out of the browser cache. Next.js already sends private, no-cache, no-store for App Router pages that read cookies and for getServerSideProps pages, so the bug usually comes from adding public or s-maxage yourself, or from a static page.