Datos sensibles en React: la lección de la franja negra de Google
Una franja negra mal aplicada en un documento público mostró lo que Google quería ocultar: cuánta agua y cuánta energía debe consumir el data center de Lincoln, en Nebraska. Según el reportaje de 1011 Now, la información que debía estar oculta seguía en el archivo. Parece cosa de oficina de trámites, pero no lo es. Es el mismo error que muchos devs cometen todos los días con datos sensibles en React. Yo también lo cometí.
Qué pasó en Lincoln
El caso es simple. Hay un proyecto de data center de Google en Lincoln. La ciudad y la compañía eléctrica local tienen que publicar documentos sobre el acuerdo. Las cifras de consumo de agua y electricidad se trataban como confidenciales, así que alguien les puso franjas negras encima.
El problema es que la franja estaba solo encima. El texto de abajo seguía ahí, legible para quien supiera mirar. Resultado: la nota que iba a tratar sobre "lo que no sabemos" se convirtió en "mira lo que se puede ver". Y quedaron más preguntas que respuestas, porque ahora todos quieren saber por qué esas cifras estaban ocultas.
No voy a entrar en el debate sobre el consumo de agua de los data centers. Es importante, pero lo que me interesa aquí es la falla técnica. Es idéntica a algo que veo seguido en code review.
Por qué falla una franja negra
En un PDF, dibujar un rectángulo negro sobre un texto no borra el texto. Son dos capas: el contenido original y un dibujo encima. El lector de PDF muestra la franja, pero el texto sigue en el archivo. Lo seleccionas, lo copias, lo pegas en el bloc de notas y listo.
Censurar de verdad elimina el contenido del archivo. Las herramientas serias lo hacen y además limpian metadatos, historial de revisiones y capas ocultas. Quien solo dibuja el cuadrado está decorando.
Ocultar en pantalla no es lo mismo que no enviar.
Guarda esta frase. Vale para un PDF y vale mucho más para tu frontend.
Datos sensibles en React: la franja negra del frontend
En React, la "franja negra" tiene varias formas. La más clásica es esta:
function UserCard({ user }: { user: User }) {
return (
<div>
<h2>{user.name}</h2>
{user.role === 'admin' && <p>CPF: {user.cpf}</p>}
</div>
)
}
Lo miras y piensas: "el CPF solo aparece para el admin". En pantalla, es cierto. Pero el objeto user completo llegó al navegador. Abres React DevTools, haces clic en el componente y el CPF está ahí en las props. O abres la pestaña Network y lees el JSON de la API.
Otras versiones del mismo error que ya me encontré por ahí:
display: noneohiddenen un bloque con datos que el usuario no debería ver.- Un endpoint
/api/users/:idque devuelve el registro completo de la base de datos, incluidopassword_hash, y el frontend solo usa tres campos. - Una feature flag que oculta un botón, pero la ruta de la acción sigue abierta en el backend.
- Una variable de entorno con prefijo
NEXT_PUBLIC_oVITE_que guarda una clave que debería ser secreta.
En todos estos casos, la interfaz es la franja. El dato está en el archivo, o sea, en el bundle, en el payload, en el HTML.
El caso que atrapa hasta a devs con experiencia: Server Components
Con React Server Components, el problema se volvió más sutil. Mucha gente cree que todo lo que corre en el servidor se queda en el servidor. Se queda, hasta que lo pasas a un Client Component.
// app/perfil/page.tsx (Server Component)
export default async function Page() {
const user = await db.user.findUnique({ where: { id } })
return <ProfileForm user={user} /> // ProfileForm tiene 'use client'
}
Todo lo que va como prop a un componente con 'use client' se serializa y se envía al navegador. El objeto completo. Si user tiene password_hash, stripe_customer_id e internal_notes, todo eso está en el payload del RSC. Se ve en el HTML de la página, en los chunks del flight. En el Pages Router pasaba lo mismo con getServerSideProps: todo terminaba en __NEXT_DATA__.
La corrección es tan simple que aburre. Selecciona solo lo que la pantalla necesita:
const user = await db.user.findUnique({
where: { id },
select: { name: true, email: true, avatarUrl: true },
})
return <ProfileForm user={user} />
Si usas Drizzle, Prisma o SQL puro, la idea es la misma: el SELECT * es el rectángulo negro dibujado encima.
Cómo evitar la filtración en tu proyecto React
No hace falta una herramienta mágica. Hacen falta algunas reglas que todo el equipo siga.
1. Arma un DTO en la frontera. Cada vez que un dato sale del servidor, pasa por una función que elige los campos. Puede ser un pick simple o un schema de Zod con .parse(), que descarta lo que no está declarado.
const PublicUser = z.object({
name: z.string(),
avatarUrl: z.string().url().nullable(),
})
return PublicUser.parse(user) // los campos extra desaparecen
2. Marca lo que nunca puede ir al cliente. El paquete server-only rompe el build si un módulo de servidor se importa en un Client Component. Y React tiene las APIs de taint (experimental_taintObjectReference y experimental_taintUniqueValue), que Next.js expone con la flag taint: true. Hacen que React lance un error si ese objeto o valor intenta cruzar al cliente.
import { experimental_taintObjectReference } from 'react'
const user = await getUserFromDb(id)
experimental_taintObjectReference('No pases el usuario crudo al cliente', user)
No es una bala de plata. Si copias el campo a otro objeto, el taint no lo sigue. Pero atrapa el error más común, que es pasar el registro completo sin pensar.
3. La autorización vive en el backend. Ocultar el botón de "eliminar" a quien no es admin es UX. Bloquear el DELETE en el servidor es seguridad. Las dos cosas hacen falta, pero solo una protege algo.
4. Haz la prueba del curioso. Antes de mandar una feature a producción, abre la pestaña Network, mira el payload de cada request y pregúntate: "¿estaría tranquilo si esto apareciera en una captura en X?". Lleva dos minutos. Es el equivalente a seleccionar el texto bajo la franja y ver si se pega.
Qué cambia esta historia en tu día a día
El caso de Lincoln es un buen recordatorio de que una filtración no siempre viene de un hacker sofisticado. A veces viene de alguien que confió en la capa visual. En el PDF fue un rectángulo. En React es un &&, un hidden, una prop de más.
Creo, y esta es la opinión fuerte del texto, que el ecosistema React facilitó demasiado este error en los últimos años. La frontera entre servidor y cliente se volvió invisible a propósito, para ser ergonómica. Eso es genial para la productividad y pésimo para quien no sabe por dónde pasa la línea. El 'use client' al inicio del archivo es, en la práctica, una aduana. Todo lo que la cruza queda declarado ante el mundo.
Así que trátalo así. Antes de pasar una prop de un Server Component a un Client Component, pregúntate qué hay dentro. Antes de devolver un JSON, revisa los campos. Y desconfía de cualquier dato que "solo no aparece en pantalla".
Google va a sobrevivir a la vergüenza. Tu app quizás no sobreviva a una filtración de CPF. Si quieres ver cómo organizo esta frontera entre servidor y cliente en mis proyectos, échale un vistazo a los proyectos que tengo publicados.
Resumen para LinkedIn
Una franja negra mal aplicada expuso lo que Google quería ocultar sobre el data center de Lincoln. El texto seguía en el PDF. Solo tenía un rectángulo dibujado encima. En React cometemos el mismo error todos los días: un `&&` que oculta el CPF en pantalla, pero manda el objeto entero al navegador. Con Server Components empeoró. Todo lo que va como prop a un `'use client'` viaja junto en el payload. Ocultar en pantalla no es lo mismo que no enviar. DTO en la frontera, `select` solo de lo necesario y autorización en el backend. Escribí sobre esto en el blog, con código y un checklist rápido. Valen los 5 minutos antes del próximo deploy. #React #NextJS #SeguridadInformática #DesarrolloWeb #FrontEnd