El centro de datos de Google y la fuga de datos en React
Alguien en Lincoln, Nebraska, pasó una franja negra sobre un documento del centro de datos de Google y creyó que había resuelto el problema. No fue así. Las cifras de consumo de agua y de energía eléctrica que debían quedar ocultas seguían accesibles, y la prensa local fue tras ellas. Lo cuenta el reportaje de 10/11 NOW, que terminó con más preguntas que respuestas. Parece una historia de ayuntamiento, pero es exactamente el mismo error que veo cada semana en código React. Es la fuga de datos en React más común que existe: ocultar en pantalla algo que ya se entregó al navegador.
Qué pasó en Lincoln
El contexto es simple. Los centros de datos consumen mucha agua para refrigeración y mucha energía. Las ciudades que reciben estos proyectos quieren saber cuánto. Las empresas prefieren no contarlo, porque el consumo de recursos dice mucho sobre su capacidad y sus planes de expansión.
En medio de eso, un documento público salió con fragmentos marcados como confidenciales. Pero la tachadura se hizo mal: la información seguía ahí, tapada visualmente, pero recuperable. Resultado: las cifras que debían quedar entre la ciudad y Google acabaron en el periódico.
No voy a repetir los valores aquí, y tampoco es lo que importa a quien programa. Lo que importa es el mecanismo del fallo. Tiene un nombre conocido en seguridad de la información: confundir ocultar con eliminar.
Si el dato llegó al cliente, es del cliente. La franja negra es solo decoración.
Por qué es el mismo bug de tu front-end
Piensa en cómo funciona una franja negra mal hecha en un PDF. Existe la capa de texto y existe un rectángulo dibujado encima. Quien mira la página ve negro. Quien selecciona, copia o abre el archivo en un editor ve el texto entero.
Ahora piensa en un componente React así:
function UserCard({ user }: { user: User }) {
return (
<div>
<h2>{user.name}</h2>
{user.role === 'admin' && <p>CPF: {user.cpf}</p>}
</div>
)
}
Para el usuario común, el CPF (el número de identificación fiscal en Brasil) no aparece. ¿Misión cumplida? No. Si el objeto user llegó entero desde la API, el CPF está en el JSON de la respuesta. Está en la pestaña Network. Está en React DevTools, en las props del componente. El && es el rectángulo negro dibujado sobre el texto.
Lo mismo vale para:
display: noneohiddenen algo que no debería existir en el HTML- filtrar en el front una lista que la API devolvió completa
- "ocultar" un botón de admin en lugar de bloquear la ruta en el servidor
- un campo de precio de coste que viene en el payload y simplemente no se renderiza
Una vez me encontré un panel de e-commerce donde el listado de productos devolvía el margen de beneficio y el nombre del proveedor a cualquier visitante. La pantalla mostraba solo nombre y precio. El JSON mostraba el negocio entero. Nadie había hecho nada malicioso. Solo un SELECT * y un res.json(produtos).
Los Server Components lo volvieron más sutil
Con React Server Components y el App Router de Next.js, mucha gente se relajó. "El componente corre en el servidor, así que es seguro". En parte. El problema vive en la frontera entre servidor y cliente.
Cuando un Server Component pasa props a un Client Component, esas props se serializan y se envían en el payload RSC. Todo lo que pasas va incluido. Mira este caso:
// page.tsx (Server Component)
export default async function Page() {
const user = await db.user.findUnique({ where: { id } })
return <ProfileForm user={user} />
}
// ProfileForm.tsx
'use client'
export function ProfileForm({ user }) {
return <input defaultValue={user.name} />
}
El formulario solo usa name. Pero el objeto user entero, con passwordHash, stripeCustomerId y lo que haya en la tabla, terminó en el HTML de la página. Abre el código fuente y busca self.__next_f.push. Ahí está, en texto plano.
Es el tipo de fuga que ninguna prueba visual detecta. La pantalla está perfecta. El snapshot pasa. QA aprueba. Y el hash de la contraseña está en el HTML.
Cómo evitar la fuga de datos en React en la práctica
La regla es aburrida y funciona: el servidor decide qué sale, no el componente. En la práctica, eso se traduce en algunos hábitos.
1. Arma DTOs explícitos. Nunca pases el registro de la base de datos directo al cliente. Elige los campos.
const user = await db.user.findUnique({
where: { id },
select: { id: true, name: true, avatarUrl: true },
})
Con Prisma, Drizzle o SQL a mano, el principio es el mismo: select explícito. Si mañana alguien agrega una columna documento a la tabla, no se filtra sola.
2. Separa el código que solo puede correr en el servidor. El paquete server-only rompe el build si un módulo de acceso a datos se importa en un Client Component.
// lib/data/users.ts
import 'server-only'
export async function getPublicProfile(id: string) {
// ...
}
Es una línea. No cuesta nada. Y convierte una fuga silenciosa en un error de build.
3. Conoce las APIs de taint. React tiene experimental_taintObjectReference y experimental_taintUniqueValue, que marcan un objeto o un valor como prohibido para cruzar al cliente. Si alguien intenta pasarlo como prop, da error. Todavía son experimentales, así que las trato como red de seguridad, no como estrategia principal. El DTO sigue siendo la primera línea.
4. Autorización en el servidor, siempre. Ocultar el botón de eliminar es UX. Bloquear el DELETE en la Server Action o en la ruta es seguridad. Necesitas las dos cosas, pero solo una protege algo.
La prueba de cinco minutos que hago en todo proyecto
No hace falta ninguna herramienta cara. Abre la aplicación con la sesión del usuario de menor permiso y haz esto:
Ctrl+Uen la página y busca palabras comopassword,hash,cpf,token,secret,cost.- Pestaña Network, filtra por Fetch/XHR, abre cada respuesta y lee el JSON entero, no solo lo que aparece en pantalla.
- React DevTools, haz clic en los componentes principales y revisa las props.
En proyectos legados, esta prueba casi siempre encuentra algo. La última vez que la hice en un sistema que heredé, encontré el correo de todos los demás usuarios de una organización viniendo junto en un listado de comentarios. La pantalla mostraba solo el nombre de pila. Es la franja negra de Lincoln, versión JavaScript.
Se puede automatizar una parte con una prueba sencilla:
const html = await fetch('http://localhost:3000/perfil').then(r => r.text())
for (const prohibido of ['passwordHash', 'stripeCustomerId']) {
if (html.includes(prohibido)) throw new Error(`Se filtró: ${prohibido}`)
}
No es bonito. Atrapa lo obvio. Y lo obvio es lo que más se filtra.
Lo que la historia de Google enseña a quien escribe código
El caso de Lincoln tiene un lado político, sobre la transparencia de las big tech y el uso de recursos públicos. Ese debate es legítimo y, sinceramente, creo que el consumo de agua de un centro de datos debería ser público por defecto. Una ciudad que cede agua y energía tiene derecho a saber cuánto.
Pero lo que me interesa aquí es el lado técnico, y es universal. Alguien tenía un dato sensible, quería protegerlo y aplicó la protección en la capa de presentación. Funcionó visualmente. Falló de verdad.
En React, esa tentación es constante porque el modelo mental es visual. Piensas en componentes, en pantallas, en lo que aparece. Un condicional de renderizado parece control de acceso. No lo es. Un Server Component parece una caja fuerte. Es una caja fuerte con una ventana llamada props.
Mi opinión: la mayoría de las fugas que veo en apps React no vienen de ataques sofisticados. Vienen de SELECT * más JSON.stringify. Es pereza en la frontera, no falta de criptografía. Y la solución tampoco es sofisticada: elegir campos, marcar módulos como server-only y revisar el payload de vez en cuando con ojos de quien quiere encontrar algo.
Si el ayuntamiento hubiera borrado el texto en lugar de pintarlo encima, no habría noticia. Si tu select tuviera tres campos en lugar de veinte, tampoco. Si quieres ver cómo organizo esa frontera entre servidor y cliente en proyectos reales, échale un vistazo a mis proyectos.
Resumen para LinkedIn
Alguien pintó una franja negra en un documento del centro de datos de Google y creyó que había ocultado los números. No lo hizo.
En Lincoln, los datos de consumo de agua y energía seguían accesibles debajo de la franja, y la prensa local fue tras ellos.
Veo ese mismo error cada semana en React: `{isAdmin && user.cpf}` oculta el dato en pantalla, pero el JSON ya se lo entregó todo al navegador.
Con Server Components es aún más sutil. Pasas el `user` entero como prop y el hash de la contraseña termina en el HTML de la página.
Si el dato llegó al cliente, es del cliente. Ocultar no es eliminar.
Mi regla es esta: el servidor decide qué sale. `select` explícito, `server-only` y, de vez en cuando, un Ctrl+U con ojos de quien quiere encontrar problemas.
Escribí el paso a paso completo en el blog, con la prueba de cinco minutos que hago en todo proyecto. Enlace en los comentarios.
#React #NextJS #SeguridadInformática #DesarrolloWeb #JavaScript