Your React Server Component Could Be Sending Sensitive Data to the Browser
September 20, 2026 · by Super Admin

Your AI just put your user's entire database record in the browser.
Sounds impossible, right?
After all, you're using a React Server Component. The component runs on the server, your database query happens on the server, and the browser only sees the final UI.
Except that's not quite how it works.
Your component might display only three fields:
- Name
- Profile photo
But suppose your database query fetched an entire user record containing 20 fields.
That record might also contain a password hash, internal role information, account flags, billing identifiers, or other information that should never be exposed to the client.
If that data crosses the Server Component boundary and becomes part of the data sent to the browser, hiding those fields with CSS or simply not rendering them doesn't make them private.
The browser may already have received them.
And that's the problem.
Server Components Don't Mean "No Data Reaches the Browser"
React Server Components are rendered on the server, but that doesn't mean every piece of data used by a Server Component stays on the server.
Modern React frameworks such as Next.js need to send information to the browser so the client can construct and interact with the resulting application.
When data is passed across a server-to-client boundary, it can be serialized into the framework's transport format.
So imagine your AI writes something conceptually like this:
const user = await db.user.findUnique({
where: { id: userId }
})
return <Profile user={user} />
The query might be convenient.
It might even look perfectly harmless.
But if user contains far more information than the UI needs, you've created an unnecessary data exposure risk.
Your component might only use:
user.name
user.email
user.profilePhoto
But the object you fetched could contain much more.
The important question isn't simply:
"Does my UI display the password hash?"
The more important question is:
"Did sensitive data cross the boundary and reach the client at all?"
Those are two very different questions.
The Browser Can See Data You Don't Display
This is where developers sometimes make a dangerous assumption.
They look at their UI and think:
"The password hash isn't displayed, so the user can't access it."
But the browser doesn't only receive what you can see on the screen.
A user can open their browser's developer tools and inspect network traffic, application state, serialized data, and other information delivered to the client.
If sensitive information was sent to the browser, removing it from the visual interface doesn't make it secret.
It's already there.
Think of your UI as a window.
You might be looking through the window at a table containing three objects. That doesn't mean there aren't seventeen other objects sitting behind the wall.
If those objects were never supposed to leave the server, don't send them to the browser in the first place.
1. Don't Fetch the Entire Database Row
The first rule is simple:
Only retrieve the fields your component actually needs.
Instead of asking your database for an entire user record, explicitly select the fields required by the UI.
Conceptually:
const user = await db.user.findUnique({
where: { id: userId },
select: {
name: true,
email: true,
profilePhoto: true
}
})
Now the resulting object contains only the information required by the component.
This is better than fetching the entire record and hoping that unused properties don't accidentally make their way into the response.
It's also easier to review.
When another developer opens the code six months later, they can immediately see which fields this particular piece of the application needs.
2. Create Explicit Data Transfer Objects
The problem can become more complicated when you have nested components.
Imagine a parent component receives a large user object and then passes that object to three different child components.
Something like:
<Profile
user={user}
/>
The profile component might only need the name.
Another component might need the email.
A third component might need the profile photo.
Passing the entire object everywhere makes it difficult to understand exactly what information each component requires.
Instead, create smaller data objects at component boundaries.
For example:
const profileData = {
name: user.name
}
const contactData = {
email: user.email
}
const avatarData = {
profilePhoto: user.profilePhoto
}
Now each component receives the smallest useful representation of the data.
This approach is often called using a Data Transfer Object, or DTO.
The idea is straightforward: don't pass a giant object around when a small, purpose-built object will do.
3. Be Careful With Nested Server and Client Components
The distinction between Server Components and Client Components is particularly important here.
A Server Component can safely access server-side resources such as a database, but once you pass data into a Client Component, you need to think carefully about what you're sending.
For example, this is potentially problematic:
const user = await getUser()
return <ClientProfile user={user} />
If user contains sensitive properties, you're potentially sending more information to the client than the component requires.
Instead, create a deliberately limited object:
const user = await getUser()
return (
<ClientProfile
name={user.name}
email={user.email}
profilePhoto={user.profilePhoto}
/>
)
Now the boundary is explicit.
The Client Component gets the information it needs and nothing more.
The RSC Payload Isn't Just HTML
Another important point is that React Server Components don't simply send a traditional HTML page and forget about everything else.
The React Server Components architecture uses a structured transport format commonly referred to as the RSC payload.
You don't need to understand every detail of that format to understand the security lesson.
The important part is this:
Data crossing the server/client boundary can become part of information delivered to the browser.
And anything delivered to the browser should generally be considered accessible to the user.
You should not treat the transport format as a secret storage mechanism.
It isn't.
An attacker doesn't need to break into your database if you've already sent the information to their browser.
The Real Problem Is Data Exposure
This isn't necessarily a database vulnerability.
Your database might be perfectly secured.
Your authentication might be working correctly.
Your authorization rules might be correct.
And yet you can still accidentally expose sensitive information by fetching too much data and passing it across the application boundary.
That's why data minimization matters.
If a component needs three fields, give it three fields.
If a client-side component needs two values, send two values.
Don't fetch twenty fields simply because it's convenient and assume the other eighteen don't matter.
They matter if they leave the server.
How to Audit Your Application
If you're using AI to generate parts of your application, this is especially worth checking.
AI-generated code often optimizes for convenience. Fetching a complete object can be simpler than carefully selecting individual fields.
So audit your Server Components and ask a few straightforward questions:
What data does this component fetch?
Is it retrieving an entire database record when it only needs a few properties?
What data crosses the server/client boundary?
Look at the props being passed to Client Components and make sure they don't contain unnecessary sensitive information.
Are large objects being passed through parent components?
If so, consider creating smaller DTOs for each component boundary.
Could a browser user inspect this information?
If the answer is yes, don't put secrets or sensitive fields in it.
The Rule to Remember
Here's the simplest way to remember all of this:
Not displaying sensitive data is not the same as not sending sensitive data.
If your application sends information to the browser, you should assume that the user can inspect it.
So don't rely on the UI to hide sensitive fields.
Don't fetch an entire database record when you need three properties.
Don't pass giant objects through your component tree just because it's convenient.
Instead, select the fields you actually need, create explicit data objects at boundaries, and keep sensitive information on the server.
Your UI is a window.
But the data sent to the browser is everything behind that window.
If sensitive information is part of what you send to the browser, changing what the window displays won't make that information disappear.
The safest sensitive data is the data you never send to the client in the first place.