Voltar para o blog
ReactSegurançaPrivacidade

Vazamento de dados no React: a lição da tarja do Google

18 de outubro de 2026·6 min de leitura·Diego Horvatti

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.
  • getServerSideProps antigo 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