Fuga de datos en React: la lección de la tachadura de Google
Alguien en Lincoln, Nebraska, pasó una franja negra sobre un documento del centro de datos de Google, y la franja no ocultó nada. Las cifras de consumo de agua y energía que debían quedar en secreto quedaron a la vista de cualquiera que quisiera leerlas. La historia salió en 1011 Now y dejó en la ciudad más preguntas que respuestas. Puede parecer un tema municipal, pero es el mismo bug que veo cada semana en código React. El nombre técnico es fuga de datos en React: la interfaz oculta la información y el payload lo entrega todo.
Qué pasó en Lincoln
Google tiene un centro de datos en Lincoln. Un centro de datos consume mucha agua para refrigeración y mucha electricidad, y quien vive cerca quiere saber cuánto. La empresa suele tratar esas cifras como secreto comercial. Por eso los documentos públicos llegan con partes tachadas.
Esta vez la tachadura se hizo mal. Según la nota, los datos ocultos quedaron accesibles y las cifras salieron a la luz. El debate local ahora gira en torno a dos cosas: el tamaño del consumo y el motivo por el que ese dato es secreto.
No voy a entrar aquí en el debate ambiental. Los detalles y las cifras están en la nota original, vale la pena leerla. Lo que me interesa es el mecanismo del fallo, porque es viejo y se repite.
En un PDF, el error clásico es dibujar un rectángulo negro encima del texto. En pantalla todo se ve negro. En el archivo, el texto sigue ahí. Basta con seleccionarlo, copiarlo y pegarlo en un editor. Les ha pasado a gobiernos, tribunales y despachos de abogados. Ahora entra en la lista un documento ligado a una de las mayores empresas tecnológicas del mundo.
Ocultar no es eliminar.
Por qué esto es un problema de dev React
El rectángulo negro es el display: none del mundo jurídico. La capa visual dice "aquí no hay nada" y la capa de datos dice lo contrario.
En React lo haces sin darte cuenta. Mira este patrón:
// UserCard.tsx
'use client'
export function UserCard({ user }: { user: User }) {
return (
<div>
<h2>{user.name}</h2>
{user.isAdmin && <span>Admin</span>}
</div>
)
}
En pantalla solo aparece el nombre. Pero el componente recibió el objeto user entero. Si ese objeto vino de la base de datos con email, cpf, passwordHash o stripeCustomerId, todo eso llegó al navegador. Está en el estado del componente, en las React DevTools y en la respuesta de red.
Nadie "renderizó" el dato sensible. Se filtró de todos modos. Es la franja negra en forma de componente.
Dónde se escapa el dato en una app con Server Components
Con React Server Components y el App Router de Next.js, el riesgo se volvió más sutil. El Server Component corre en el servidor, así que parece seguro traer ahí el registro completo. Y es seguro, hasta el momento en que pasas ese registro como prop a un Client Component.
// app/perfil/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} />
}
Toda prop que cruza la frontera 'use client' se serializa en el payload RSC. Ese payload va incrustado en el HTML, en scripts del tipo self.__next_f.push(...). Cualquiera con "Ver código fuente" puede leerlo.
Los puntos de fuga más comunes que encuentro en code review:
- Objeto entero del ORM pasado como prop a un Client Component.
getServerSidePropsantiguo devolviendo el registro completo, que termina en__NEXT_DATA__.- Respuesta de API con
SELECT *que el front solo filtra al mostrar. - Variable de entorno sin prefijo público importada en un archivo que acaba en el bundle.
- Feature flag de admin que oculta un botón, con el endpoint detrás sin verificación de permisos.
El último es mi favorito. El botón desapareció, pero el curl sigue funcionando.
Cómo evitar la fuga de datos en React en la práctica
La regla es simple: el dato que no debe llegar al cliente no puede salir del servidor. Ocultarlo con CSS o con renderizado condicional no cuenta.
1. Selecciona solo lo que la pantalla necesita
La forma más barata es cortar en la consulta:
const user = await db.user.findUnique({
where: { id },
select: { name: true, avatarUrl: true },
})
Si el campo no se trajo, no tiene cómo filtrarse. Una línea de select resuelve más que cualquier librería de seguridad.
2. Crea un DTO en la frontera
Cuando el mismo registro alimenta varias pantallas, centraliza la conversión:
// lib/dto.ts
import 'server-only'
export function toPublicUser(u: UserRow) {
return { id: u.id, name: u.name, avatarUrl: u.avatarUrl }
}
El import 'server-only' hace que el build falle si alguien importa ese archivo en un Client Component. Es una alarma gratis.
3. Usa las APIs de taint de React
React tiene dos APIs experimentales pensadas justo para este escenario. Marcas un objeto o un valor como prohibido para cruzar al cliente:
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } })
experimental_taintObjectReference(
'No pases el registro completo del usuario al cliente',
user,
)
experimental_taintUniqueValue(
'El token de API no sale del servidor',
user,
user.apiToken,
)
return user
}
En Next.js, actívalo con experimental: { taint: true } en el next.config. Si alguien intenta pasar user o el token como prop a un Client Component, React lanza un error con tu mensaje.
Una advertencia honesta: taint es una red de seguridad, no un muro. Si copias los campos a un objeto nuevo ({ ...user }), la referencia marcada se pierde. Por eso existe taintUniqueValue para valores críticos, y por eso el select sigue siendo la primera línea de defensa.
4. Revisa lo que realmente se envió
No confíes en lo que muestra la pantalla. Revisa lo que viajó:
curl -s https://tuapp.com/perfil | grep -oE 'email|cpf|apiToken|passwordHash'
Si aparece algo, tienes una franja negra mal puesta. Haz lo mismo en la pestaña Network de las DevTools, mirando la respuesta de las rutas de API y el payload RSC de las navegaciones client-side.
Puedes convertir esto en un test. Un Playwright simple que abre la página, toma el HTML y verifica que no aparezca ninguna cadena sensible ya atrapa la mayoría de las regresiones.
"Pero es solo un campo, ¿quién lo va a mirar?"
Esa objeción la escucho mucho. La respuesta está en Lincoln. Alguien miró.
Los documentos públicos los leen periodistas, activistas y curiosos. Los payloads de API los leen scrapers, investigadores de seguridad y competidores. Mirar no cuesta nada. Filtrar puede costar una noticia con el nombre de tu empresa en el titular.
Y hay un detalle que pesa más: el dato filtrado no vuelve. Puedes corregir el deploy en cinco minutos, pero el HTML ya fue cacheado, archivado y copiado. Google no puede "desfiltrar" el consumo de agua de Lincoln. Tú tampoco puedes desfiltrar el número de documento de tu usuario.
La lección que queda
El error de Lincoln no fue de criptografía ni de infraestructura sofisticada. Fue confiar en la capa visual para proteger un dato. En el front lo hacemos todo el tiempo, porque React hace muy fácil pasar un objeto entero y mostrar solo una parte.
Mi opinión, sin rodeos: en una app con Server Components, pasar un registro de la base de datos directo como prop a un Client Component debería tratarse como bug en code review, siempre. Corta en la consulta, arma el DTO, activa el taint y corre el curl antes del deploy. ¿Da trabajo? Un poco. Mucho menos que explicarle al equipo legal por qué el hash de la contraseña estaba en el código fuente de la página.
Si quieres ver cómo organizo esa frontera entre servidor y cliente en proyectos reales, échale un vistazo a mis proyectos.
Resumen para LinkedIn
La franja negra de Google en Lincoln no ocultó nada. Y puede que tu componente React tampoco oculte nada. Un documento sobre el centro de datos llegó con las cifras de agua y energía tachadas, pero se podía seleccionar el texto y copiarlo todo. En el front cometemos el mismo error cada semana: pasamos el objeto entero de la base de datos como prop, mostramos solo el nombre, y el número de documento viaja igual en el payload. Ocultar no es eliminar. Si el dato no puede llegar al cliente, no puede salir del servidor. Lo que yo hago: select solo con los campos que usa la pantalla, DTO con server-only, taint de React y un curl al HTML antes del deploy. Escribí el paso a paso completo en el blog. Vale la pena revisar hoy mismo el código fuente de tu página. #React #NextJS #SeguridadInformática #DesarrolloWeb #ProtecciónDeDatos