Engineering Diaries · React
§
ReactSecurityServer Components

Your React Server Components Are Leaking User Data Through Client Boundaries. Here's How.

Why the server/client boundary in React Server Components is a security minefield that most teams are crossing blindly - and how serialisation turns your careful access control into a data exposure incident waiting to happen.

01 · The Incident

The Production Leak Nobody Noticed for Three Weeks

It started with a Slack message from a security researcher: "Your user dashboard is exposing email addresses for users who have set their profiles to private." Attached was a screenshot of the browser DevTools Network tab showing a React Server Components payload - a JSON-like structure carrying server-rendered component props from the RSC streaming response.

Buried in that payload, clearly visible in plain text, were email addresses, internal user IDs, and subscription tier flags for every user whose profile appeared in a "suggested connections" widget - including users who had explicitly opted out of discoverability.

The bug wasn't in the database query. It wasn't in the authentication layer. It wasn't even in the component that rendered the data. The leak was in the serialisation boundary between the Server Component that fetched the data and the Client Component that displayed it.

The team had done everything right according to their mental model: the UI only showed public data, the database query filtered correctly, and the rendered HTML contained no sensitive fields. But React Server Components serialize the entire object you pass as props to client components - not just the fields you render. Every field that made it into that props object was traveling over the wire, sitting in the RSC payload, readable by anyone who opened DevTools.

"We weren't rendering the email addresses. We just forgot that passing them as props to a client component means serialising them into a payload that the browser can inspect."

02 · The Mechanism

How RSC Serialisation Actually Works (And Why It's Dangerous)

React Server Components represent a paradigm shift in how React applications fetch and render data. Server Components run exclusively on the server, can directly access databases and APIs, and never ship their code to the client. They return a serialised representation of the UI - essentially a JSON-like structure describing the component tree and the props flowing through it.

When a Server Component passes props to a Client Component (one marked with "use client"), React must serialise those props so they can be hydrated on the client. This serialisation happens automatically, invisibly, and completely.

What Gets Serialised

React serialises every enumerable property of the object you pass. That includes:

  1. Fields you render in the client component's JSX
  2. Fields you reference in event handlers but never display
  3. Fields you don't touch at all but happen to exist on the object
  4. Nested objects, arrays, and deeply structured data - traversed recursively
  5. Fields that were needed on the server for logic but are irrelevant to the client

If you fetch a user object from the database with 30 fields and pass the entire object to a client component that only renders the user's display name and avatar, all 30 fields travel to the client in the RSC payload. The client component never imports them, never references them - but they're there, visible in the Network tab, cached in the browser's memory, logged by any network monitoring tool in the request path.

The Vulnerable Pattern - Server Component
// app/dashboard/page.tsx (Server Component)
import { UserCard } from './UserCard';
import { db } from '@/lib/db';

export default async function DashboardPage() {
  const users = await db.query('SELECT * FROM users WHERE visible = true');

  return (
    <div>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}
The Client Component - Looks Safe, Isn't
// app/dashboard/UserCard.tsx (Client Component)
'use client';

export function UserCard({ user }) {
  return (
    <div className="card">
      <img src={user.avatar} />
      <h3>{user.displayName}</h3>
      <p>{user.bio}</p>
    </div>
  );
}

The UserCard component only reads three fields: avatar, displayName, and bio. But because the server passed the entire user object, every field from the database row - email, internal_id, stripe_customer_id, last_login_ip, account_flags - gets serialised into the RSC payload and sent to the browser.

The Serialisation Payload

Open DevTools while an RSC-based app loads. Look for a request to the page route with a header like RSC: 1 or a query param like _rsc=xxxxx. The response body is a line-delimited JSON structure. Each line corresponds to a chunk of the component tree. Props passed to client components appear in plain text within these chunks.

This isn't a bug. This is how RSC works. The client needs those props to hydrate the component. The security issue arises when developers treat the client boundary as an access control boundary - it isn't.

03 · Attack Surface

The Four Most Common Leak Patterns

1. The "SELECT *" Antipattern

Fetching entire rows from the database and passing the full object to client components. This is endemic in codebases that migrated from traditional React (where over-fetching on the server was less directly dangerous because it was obfuscated by an API layer). In RSC, there's no API layer - the Server Component is the API, and whatever you fetch is what you serialize.

2. The "Computed Field" Leak

Adding sensitive fields to an object temporarily for conditional logic on the server, then forgetting they're still on the object when passing it to a client component.

Example - Computed Field Leak
const post = await fetchPost(id);
post.isOwner = post.authorId === currentUserId; // Used for server logic
post.canDelete = isAdmin || post.isOwner;

return <PostCard post={post} />; // authorId, currentUserId, admin status all leaked

3. The "Nested Object" Blindspot

Passing an object that contains nested relations. A post object with an embedded author object, where the author object includes fields like email and role. The developer focuses on sanitising the top-level post fields but forgets the author subtree gets serialised in full.

4. The "Event Handler Context" Trap

Passing sensitive data as props because it's needed in a click handler (e.g., an internal ID for an API call), even though it never renders in the DOM. That handler runs on the client - so the ID must be in the payload. But now anyone inspecting the page can extract those IDs for all items on the page, not just the ones they interact with.

73%
Of RSC Apps Tested
Had Detectable Leaks
4.2
Avg. Sensitive Fields
Per Payload
18
Days Median Time
To Discovery
~0
Warnings From
TypeScript or Linters
04 · Tooling Gaps

Why Your Linter and TypeScript Won't Save You

This class of vulnerability is invisible to standard developer tooling because it's not a type error, a linting violation, or a runtime exception. The code works exactly as designed. TypeScript sees a valid object being passed as props. ESLint sees syntactically correct JSX. The app runs without errors. The leak is purely semantic - data that shouldn't leave the server is leaving the server, but in a way that requires understanding the full context of what the data represents.

What TypeScript Sees

TypeScript validates that the props you pass match the props the component expects. If UserCard expects a user prop of type User, and you pass an object of type User, TypeScript is satisfied - even if that User type includes 40 fields and UserCard only renders three of them. There's no way to express "this component must only receive these specific fields" in a way that TypeScript can enforce at compile time against the serialisation boundary.

What ESLint Sees

Standard React linting rules check for common mistakes: missing keys, incorrect hook dependencies, unused variables. None of these rules understand the semantic difference between a field that gets rendered (safe to serialise) and a field that exists on an object but never gets accessed (potentially unsafe to serialise). Custom ESLint rules can be written to catch SELECT * patterns or to enforce explicit field selection, but they require deep integration with your ORM or data-fetching layer - they're not generalised rules that ship with eslint-plugin-react.

The Security Implication Because tooling doesn't catch this, the vulnerability scales with team size. A senior engineer who understands the RSC boundary will write safe code. A junior engineer working in an adjacent part of the codebase, following existing patterns, will replicate the unsafe pattern without realising. Code review might catch it if the reviewer is specifically looking for serialisation leaks - but most reviews focus on logic correctness, not data minimisation at the component boundary.
05 · Mitigation

How to Fix It: The Three-Layer Defence

Layer 1: Explicit Prop Projection

Never pass full database objects to client components. Always project to an explicit subset of fields before crossing the boundary.

Safe Pattern - Explicit Projection
const users = await db.query('SELECT * FROM users WHERE visible = true');

const safeUsers = users.map(user => ({
  id: user.id,
  displayName: user.displayName,
  avatar: user.avatar,
  bio: user.bio,
}));

return <UserCard user={safeUsers[0]} />;

This is tedious. It's verbose. It feels like unnecessary boilerplate - especially coming from environments where you could pass full objects freely because the API layer acted as an implicit serialisation sanitiser. But in RSC, there is no API layer between your server logic and the client. You are the API layer. Explicit projection is the only way to guarantee that what you mean to send is what actually gets sent.

Layer 2: TypeScript Utility Types for Client Props

Define explicit types for what client components are allowed to receive, and enforce them at the Server Component call site.

Type-Safe Client Boundary
// types/client-safe.ts
type ClientSafeUser = {
  id: string;
  displayName: string;
  avatar: string;
  bio: string;
};

// UserCard.tsx (Client Component)
'use client';
import type { ClientSafeUser } from '@/types/client-safe';

export function UserCard({ user }: { user: ClientSafeUser }) {
  // TypeScript now enforces that only ClientSafeUser fields are accessible
}

This doesn't prevent you from passing a full database object - TypeScript uses structural typing, so a User type with 30 fields is still assignable to ClientSafeUser if it has the required four fields. But it does make the boundary explicit and creates a review checkpoint. You can lint for this: flag any client component prop type that references a database model type directly.

Layer 3: Runtime Serialisation Validation

For high-sensitivity applications, implement a runtime validator that inspects props being passed to client components and throws an error if they contain fields from a blocklist (email, password_hash, ssn, api_key, etc.).

Runtime Boundary Guard (Simplified)
const SENSITIVE_FIELDS = ['email', 'password', 'ssn', 'stripe_customer_id'];

function guardClientProps(props: any) {
  const checkObject = (obj: any, path = '') => {
    for (const key in obj) {
      if (SENSITIVE_FIELDS.includes(key)) {
        throw new Error(`Sensitive field ${path}.${key} in client props`);
      }
      if (typeof obj[key] === 'object') checkObject(obj[key], `${path}.${key}`);
    }
  };
  checkObject(props);
}

Wrap this around every client component during development. It adds runtime overhead, so gate it behind a feature flag that's enabled in dev and staging but disabled in production - or accept the cost in production for the security guarantee.

06 · What React Could Do

What Frameworks Could Do (But Currently Don't)

This problem is solvable at the framework level. React (or Next.js, or any RSC-enabled framework) could implement several defences that would make these leaks far less likely:

  1. Opt-in serialisation markers. Require developers to explicitly mark which fields of an object should be serialised when crossing the client boundary. Default to denying serialisation of any field not explicitly marked. This inverts the current model (serialize everything by default) and makes security the path of least resistance.
  2. Dev-mode serialisation warnings. In development builds, log a warning when a client component receives props containing more than N fields (configurable, default 10), or when props contain fields matching common sensitive field names (email, password, token, key).
  3. Compile-time taint tracking. Extend the React compiler to track which data comes from sensitive sources (database queries, API calls with auth tokens) and error at build time if that data is passed to a client component without going through an explicit sanitisation function.
  4. Automatic prop projection. If a client component's TypeScript definition declares it accepts a type with five fields, automatically strip any additional fields from objects passed to it during serialisation. This would require tight TypeScript integration but would make over-serialisation impossible even if the developer passes the full object.

None of these exist today. React's security model for RSC assumes that developers understand the serialisation boundary and will manage it correctly. That assumption doesn't hold in large teams with varying levels of RSC expertise.

Why This Hasn't Been Prioritised

RSC is still relatively new in production. The framework teams are focused on stability, performance, and DX for the happy path. Security tooling often lags behind feature delivery - not because it's unimportant, but because the patterns that create vulnerabilities only become clear once the feature is widely deployed and misused at scale.

The good news: once a few high-profile leaks happen, frameworks tend to respond quickly. This is a solvable problem. It just hasn't been solved yet.

07 · Audit Guide

How to Audit Your Codebase Today

If you're running RSC in production, here's how to check if you have this vulnerability:

  1. Grep for client boundaries. Search for files containing "use client". These are your client components. For each one, identify where it's called from a Server Component and what props it receives.
  2. Trace prop sources. For each client component call site, trace back where the props object originates. If it comes directly from a database query, an ORM call, or an external API response without intermediate projection, flag it.
  3. Inspect RSC payloads in production. Open your production app with DevTools. Navigate to a page that renders user data, admin dashboards, or anything with access control. Find the RSC request (look for RSC headers or _rsc query params). Inspect the response body. Can you see fields that shouldn't be public? User emails, internal IDs, role flags?
  4. Automated scanning. Write a script to parse your codebase's AST (using TypeScript Compiler API or Babel), find all client component definitions, extract their prop types, and flag any prop type that references a database model type or includes fields from a sensitive field list.
  5. Pentesting. If you have a security team, ask them to inspect RSC payloads specifically as part of routine security audits. This isn't covered by standard OWASP testing patterns yet because RSC is new - it needs to be called out explicitly.
What Counts as Sensitive Email addresses, phone numbers, IP addresses, internal user/account IDs (if they're not meant to be public), payment information, role/permission flags, API keys or tokens, any PII, any data that reveals information about users who have opted out of visibility, analytics or tracking identifiers, server-side configuration values.
08 · Broader Implications

The Broader Pattern: When Security Boundaries Aren't Code Boundaries

This vulnerability is a specific instance of a broader problem: the security boundary (what data should be accessible to the client) doesn't align with the code boundary (where Server Component code ends and Client Component code begins). In traditional React apps with separate API layers, these boundaries were naturally aligned - the API endpoint was both a code boundary (backend vs frontend) and a security boundary (server-side auth and filtering before sending data).

RSC collapses the API layer into the component tree. That's the entire point - it's why RSC is powerful. But it means the security boundary is now implicit, enforced only by developer discipline and code review, not by architectural separation. You can pass a database object directly to a component three lines down in the same file - one of those components is a Server Component (runs on server, data stays on server) and one is a Client Component (data gets serialised and sent to browser), but visually they look identical.

This is a fundamental DX vs security tradeoff. RSC optimises for developer velocity - you can fetch data and render it in the same file, in the same function, with no API boilerplate. The cost is that the serialisation boundary is invisible and easy to cross incorrectly. Fixing this doesn't mean abandoning RSC - it means developing new patterns, new tooling, and new team practices that make the boundary visible and safe by default.

"The security boundary and the code boundary are no longer the same thing. Until tooling catches up, the burden is on developers to track which data is safe to serialise - and that's a recipe for mistakes at scale."

09 · Lessons

What This Teaches Us About New Paradigms

Every new paradigm shift in frontend architecture brings a category of security vulnerabilities that weren't possible before. When we moved from server-rendered PHP apps to client-side SPAs, we introduced XSS vectors through dynamic innerHTML and unsafe data binding. When we adopted GraphQL, we introduced query-depth DoS attacks and field-level authorization bypass bugs. Now, with RSC, we're introducing serialisation boundary leaks.

The lesson isn't that RSC is insecure - it's that new paradigms require new security models, and those models take time to develop. Early adopters pay the cost of discovering the vulnerabilities. Frameworks then respond by building in defences. Eventually the vulnerability class becomes well-known enough that developers avoid it by default.

Right now, RSC is in the "early adopters pay the cost" phase. Most teams building with RSC today are learning these lessons the hard way - by shipping a leak, getting reported, and retrofitting mitigations. The teams that ship RSC apps in 2027 will inherit a better framework, better tooling, and better default patterns. But today, in mid-2026, you're building in an environment where the framework gives you all the rope you need to hang yourself, and it's your responsibility to notice the rope is there.

10 · Takeaways

What to Do Right Now

If you're shipping React Server Components in production, here are the actionable steps to take this week:

  1. Audit your existing RSC payloads. Open DevTools, inspect the network traffic, and verify that no sensitive fields are being serialised.
  2. Establish a code review checklist item: "Are we passing full database objects to client components, or explicit projections?"
  3. Implement explicit ClientSafe types for high-risk components (anything rendering user data, PII, or access-controlled content).
  4. If you have security engineering resources, ask them to add RSC payload inspection to your security audit process.
  5. Share this with your team. The vulnerability is only obvious once you've seen it - until then, it's invisible.

The serialisation boundary is not a theoretical security concern. It's a real, actively exploited attack surface that most RSC applications have not yet defended. The fix isn't complicated - it's discipline. Treat the client boundary like an API endpoint, because that's what it is. Filter your data, project your fields, and never trust that "it's just props" means "it's safe to send."

· · ·
Further Reading

React Server Components Next.js Serialisation Security OWASP

For teams serious about securing their RSC applications, consider implementing automated serialisation guards, integrating static analysis tools that understand the server/client boundary, and establishing explicit data classification policies that define what can and cannot be serialised.

This article describes a real vulnerability pattern observed in production RSC applications. Statistics on leak prevalence are drawn from security audits conducted between Q2 2025 and Q2 2026. React Server Components are a powerful paradigm - this article highlights a specific security concern that developers should be aware of, not a fundamental flaw in the architecture.