Vazamento de dados no React: a lição da tarja do Google
Alguém em Lincoln, no Nebraska, passou uma tarja preta num documento sobre o data center do Google, e a tarja não escondeu nada. Os números de consumo de água e energia que deveriam ficar em sigilo apareceram para quem quis ler. A história saiu na 1011 Now e rendeu mais perguntas do que respostas na cidade. Pode parecer assunto de prefeitura, mas é o mesmo bug que vejo toda semana em código React. O nome técnico é vazamento de dados no React: a interface esconde a informação e o payload entrega tudo.
O que aconteceu em Lincoln
O Google tem um data center em Lincoln. Data center consome muita água para resfriamento e muita eletricidade, e quem mora perto quer saber quanto. A empresa costuma tratar esses números como segredo comercial. Por isso os documentos públicos chegam com trechos tarjados.
Dessa vez a tarja foi feita do jeito errado. Pelo relato da reportagem, os dados ocultos ficaram acessíveis e os números vieram a público. A discussão local agora gira em torno de duas coisas: o tamanho do consumo e o motivo de esse dado ser sigiloso.
Não vou entrar no debate ambiental aqui. Os detalhes e os números estão na matéria original, vale a leitura. O que me interessa é o mecanismo da falha, porque ele é velho e se repete.
Em PDF, o erro clássico é desenhar um retângulo preto por cima do texto. Na tela fica tudo preto. No arquivo, o texto continua lá. Basta selecionar, copiar e colar num editor. Já aconteceu com governos, tribunais e escritórios de advocacia. Agora entrou para a lista um documento ligado a uma das maiores empresas de tecnologia do mundo.
Esconder não é remover.
Por que isso é um problema de dev React
O retângulo preto é o display: none do mundo jurídico. A camada visual diz "não tem nada aqui" e a camada de dados diz o contrário.
No React, você faz isso sem perceber. Olha esse padrão:
// UserCard.tsx
'use client'
export function UserCard({ user }: { user: User }) {
return (
<div>
<h2>{user.name}</h2>
{user.isAdmin && <span>Admin</span>}
</div>
)
}
Na tela aparece só o nome. Mas o componente recebeu o objeto user inteiro. Se esse objeto veio do banco com email, cpf, passwordHash ou stripeCustomerId, tudo isso foi para o navegador. Está no estado do componente, nas React DevTools e na resposta de rede.
Ninguém "renderizou" o dado sensível. Ele vazou mesmo assim. É a tarja preta em forma de componente.
Onde o dado escapa num app com Server Components
Com React Server Components e o App Router do Next.js, o risco ficou mais sutil. O Server Component roda no servidor, então parece seguro buscar o registro inteiro lá. E é seguro, até a hora em que você passa esse registro como prop para um 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 atravessa a fronteira 'use client' é serializada no payload RSC. Esse payload vai embutido no HTML, em scripts tipo self.__next_f.push(...). Qualquer pessoa com "Exibir código-fonte" consegue ler.
Os pontos de fuga mais comuns que encontro em code review:
- Objeto inteiro do ORM passado como prop para Client Component.
getServerSidePropsantigo devolvendo o registro completo, que vai parar no__NEXT_DATA__.- Resposta de API com
SELECT *que o front só filtra na hora de exibir. - Variável de ambiente sem prefixo público importada num arquivo que acaba no bundle.
- Feature flag de admin escondendo um botão, com o endpoint por trás sem checagem de permissão.
O último é o meu favorito. O botão sumiu, mas o curl continua funcionando.
Como evitar vazamento de dados no React na prática
A regra é simples: o dado que não deve chegar ao cliente não pode sair do servidor. Esconder com CSS ou com renderização condicional não conta.
1. Selecione só o que a tela precisa
O jeito mais barato é cortar na consulta:
const user = await db.user.findUnique({
where: { id },
select: { name: true, avatarUrl: true },
})
Se o campo não foi buscado, ele não tem como vazar. Uma linha de select resolve mais do que qualquer biblioteca de segurança.
2. Crie um DTO na fronteira
Quando o mesmo registro alimenta várias telas, centralize a conversão:
// lib/dto.ts
import 'server-only'
export function toPublicUser(u: UserRow) {
return { id: u.id, name: u.name, avatarUrl: u.avatarUrl }
}
O import 'server-only' faz o build quebrar se alguém importar esse arquivo num Client Component. É um alarme de graça.
3. Use as APIs de taint do React
O React tem duas APIs experimentais pensadas exatamente para esse cenário. Você marca um objeto ou um valor como proibido de atravessar para o cliente:
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } })
experimental_taintObjectReference(
'Não passe o registro inteiro do usuário para o cliente',
user,
)
experimental_taintUniqueValue(
'Token de API não sai do servidor',
user,
user.apiToken,
)
return user
}
No Next.js, ative com experimental: { taint: true } no next.config. Se alguém tentar passar user ou o token como prop para um Client Component, o React lança erro com a sua mensagem.
Uma ressalva honesta: taint é uma rede de proteção, não um muro. Se você copiar os campos para um objeto novo ({ ...user }), a referência marcada se perde. Por isso o taintUniqueValue existe para valores críticos, e por isso o select continua sendo a primeira linha de defesa.
4. Confira o que realmente foi enviado
Não confie no que a tela mostra. Confira o que trafegou:
curl -s https://seuapp.com/perfil | grep -oE 'email|cpf|apiToken|passwordHash'
Se aparecer alguma coisa, você tem uma tarja preta mal passada. Faça o mesmo na aba Network das DevTools, olhando a resposta das rotas de API e o payload RSC das navegações client-side.
Dá para transformar isso num teste. Um Playwright simples que abre a página, pega o HTML e verifica que nenhuma string sensível aparece já pega a maioria das regressões.
"Mas é só um campo, quem vai olhar?"
Essa objeção eu ouço bastante. A resposta está em Lincoln. Alguém olhou.
Documentos públicos são lidos por jornalistas, ativistas e curiosos. Payloads de API são lidos por scrapers, pesquisadores de segurança e concorrentes. O custo de olhar é zero. O custo de vazar pode ser uma notícia com o nome da sua empresa no título.
E tem um detalhe que pesa mais: o dado vazado não volta. Você pode corrigir o deploy em cinco minutos, mas o HTML já foi cacheado, arquivado e copiado. O Google não consegue "desvazar" o consumo de água de Lincoln. Você também não consegue desvazar o CPF do seu usuário.
O que fica de lição
O erro de Lincoln não foi de criptografia nem de infraestrutura sofisticada. Foi confiar na camada visual para proteger dado. No front a gente faz isso o tempo todo, porque o React deixa muito fácil passar um objeto inteiro adiante e mostrar só um pedaço.
Minha opinião, sem rodeio: em app com Server Components, passar registro de banco direto como prop para Client Component deveria ser tratado como bug em code review, sempre. Corte na consulta, monte o DTO, ligue o taint e rode o curl antes do deploy. Dá trabalho? Um pouco. Bem menos do que explicar para o jurídico por que o hash de senha estava no código-fonte da página.
Se quiser ver como eu organizo essa fronteira entre servidor e cliente em projetos reais, dá uma olhada nos meus projetos.
Resumo para o LinkedIn
A tarja preta do Google em Lincoln não escondeu nada. E o seu componente React talvez também não esconda. Um documento sobre o data center veio com os números de água e energia tarjados, só que dava para selecionar o texto e copiar tudo. No front a gente comete o mesmo erro toda semana: passa o objeto inteiro do banco como prop, mostra só o nome, e o CPF vai junto no payload. Esconder não é remover. Se o dado não pode chegar ao cliente, ele não pode sair do servidor. O que eu faço: select só com os campos que a tela usa, DTO com server-only, taint do React e um curl no HTML antes do deploy. Escrevi o passo a passo completo no blog. Vale conferir o código-fonte da sua página hoje mesmo. #React #NextJS #SegurançaDaInformação #DesenvolvimentoWeb #LGPD