Data leaks in React: the lesson from Google's botched redaction
Someone in Lincoln, Nebraska, put a black redaction bar over a document about Google's data center, and the bar hid nothing. The water and energy numbers that were supposed to stay confidential showed up for anyone who cared to read them. The story ran on 1011 Now and left the city with more questions than answers. It may sound like a city hall issue. But it's the same bug I see every week in React code. The technical name is a data leak in React: the UI hides the information and the payload hands over everything.
What happened in Lincoln
Google has a data center in Lincoln. Data centers use a lot of water for cooling and a lot of electricity, and people who live nearby want to know how much. The company usually treats those numbers as trade secrets. That's why public documents arrive with redacted sections.
This time the redaction was done the wrong way. According to the report, the hidden data stayed accessible and the numbers became public. The local debate now revolves around two things: how big the consumption is and why that data is secret at all.
I won't get into the environmental debate here. The details and numbers are in the original story, and it's worth a read. What interests me is how the failure works, because it's old and it keeps happening.
In PDFs, the classic mistake is drawing a black rectangle over the text. On screen it all looks black. In the file, the text is still there. Just select it, copy it and paste it into an editor. It has happened to governments, courts and law firms. Now a document tied to one of the biggest tech companies in the world joins the list.
Hiding is not removing.
Why this is a React dev problem
The black rectangle is the legal world's display: none. The visual layer says "nothing here" and the data layer says the opposite.
In React, you do this without noticing. Look at this pattern:
// UserCard.tsx
'use client'
export function UserCard({ user }: { user: User }) {
return (
<div>
<h2>{user.name}</h2>
{user.isAdmin && <span>Admin</span>}
</div>
)
}
Only the name shows up on screen. But the component received the entire user object. If that object came from the database with email, ssn, passwordHash or stripeCustomerId, all of it went to the browser. It's in the component state, in React DevTools and in the network response.
Nobody "rendered" the sensitive data. It leaked anyway. It's the black bar in component form.
Where data escapes in an app with Server Components
With React Server Components and the Next.js App Router, the risk got subtler. The Server Component runs on the server, so fetching the whole record there feels safe. And it is safe, right up until you pass that record as a prop to a Client Component.
// app/profile/page.tsx (Server Component)
import { UserCard } from './UserCard'
export default async function Page() {
const user = await db.user.findUnique({ where: { id: 1 } })
return <UserCard user={user} />
}
Every prop that crosses the 'use client' boundary gets serialized into the RSC payload. That payload is embedded in the HTML, in scripts like self.__next_f.push(...). Anyone with "View Page Source" can read it.
The most common leak points I find in code review:
- The whole ORM object passed as a prop to a Client Component.
- Old
getServerSidePropsreturning the full record, which ends up in__NEXT_DATA__. - An API response with
SELECT *that the front end only filters at display time. - An environment variable without a public prefix imported in a file that ends up in the bundle.
- An admin feature flag hiding a button, while the endpoint behind it has no permission check.
The last one is my favorite. The button is gone, but curl still works.
How to avoid data leaks in React in practice
The rule is simple: data that shouldn't reach the client can't leave the server. Hiding it with CSS or conditional rendering doesn't count.
1. Select only what the screen needs
The cheapest fix is to cut it at the query:
const user = await db.user.findUnique({
where: { id },
select: { name: true, avatarUrl: true },
})
If the field was never fetched, it can't leak. One line of select does more than any security library.
2. Create a DTO at the boundary
When the same record feeds several screens, centralize the conversion:
// lib/dto.ts
import 'server-only'
export function toPublicUser(u: UserRow) {
return { id: u.id, name: u.name, avatarUrl: u.avatarUrl }
}
The import 'server-only' breaks the build if someone imports this file in a Client Component. It's a free alarm.
3. Use React's taint APIs
React has two experimental APIs built for exactly this scenario. You mark an object or a value as forbidden from crossing over to the client:
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } })
experimental_taintObjectReference(
'Do not pass the whole user record to the client',
user,
)
experimental_taintUniqueValue(
'API token never leaves the server',
user,
user.apiToken,
)
return user
}
In Next.js, turn it on with experimental: { taint: true } in next.config. If someone tries to pass user or the token as a prop to a Client Component, React throws an error with your message.
An honest caveat: taint is a safety net, not a wall. If you copy the fields into a new object ({ ...user }), the tainted reference is lost. That's why taintUniqueValue exists for critical values. And that's why select is still the first line of defense.
4. Check what was actually sent
Don't trust what the screen shows. Check what went over the wire:
curl -s https://yourapp.com/profile | grep -oE 'email|ssn|apiToken|passwordHash'
If anything shows up, you have a badly applied black bar. Do the same in the DevTools Network tab. Look at the API route responses and the RSC payload of client-side navigations.
You can turn this into a test. A simple Playwright script that opens the page, grabs the HTML and checks that no sensitive string appears will catch most regressions.
"But it's just one field, who's going to look?"
I hear this objection a lot. The answer is in Lincoln. Someone looked.
Public documents get read by journalists, activists and curious people. API payloads get read by scrapers, security researchers and competitors. Looking costs nothing. Leaking can cost you a news story with your company's name in the headline.
And there's a detail that matters even more: leaked data doesn't come back. You can fix the deploy in five minutes, but the HTML has already been cached, archived and copied. Google can't "unleak" Lincoln's water consumption. You can't unleak your user's SSN either.
The takeaway
The Lincoln mistake wasn't about cryptography or fancy infrastructure. It was trusting the visual layer to protect data. On the front end we do this all the time, because React makes it very easy to pass a whole object along and show only a piece of it.
My take, no hedging: in an app with Server Components, passing a database record straight as a prop to a Client Component should always be flagged as a bug in code review. Cut it at the query, build the DTO, turn on taint and run the curl before you deploy. Is it extra work? A little. A lot less than explaining to legal why the password hash was sitting in the page source.
If you want to see how I handle this server/client boundary in real projects, take a look at my projects.
LinkedIn summary
Google's black redaction bar in Lincoln hid nothing. Your React component might not hide anything either. A document about the data center came with the water and power numbers blacked out. But you could select the text and copy all of it. On the front end we make the same mistake every week. We pass the whole database object as a prop, show only the name, and the SSN rides along in the payload. Hiding is not removing. If the data can't reach the client, it can't leave the server. What I do: select only the fields the screen uses, a DTO with server-only, React's taint APIs and a curl on the HTML before every deploy. I wrote the full step by step on the blog. Go check your page source today. #React #NextJS #InfoSec #WebDevelopment #DataPrivacy