Dados sensíveis no React: a lição da tarja preta do Google
Uma tarja preta mal aplicada num documento público mostrou o que o Google queria esconder: quanto de água e de energia o data center de Lincoln, no Nebraska, deve consumir. Segundo a reportagem da 1011 Now, a informação que deveria estar ocultada continuava no arquivo. Parece coisa de cartório, mas não é. Esse é o mesmo erro que muito dev comete todo dia com dados sensíveis no React. Eu também já cometi.
O que aconteceu em Lincoln
O caso é simples. Existe um projeto de data center do Google em Lincoln. A cidade e a concessionária de energia local precisam publicar documentos sobre o acordo. Os números de consumo de água e eletricidade eram tratados como confidenciais, então alguém aplicou tarjas por cima deles.
O problema é que a tarja estava só por cima. O texto embaixo continuava lá, legível para quem soubesse olhar. Resultado: a matéria que era para ser sobre "o que não sabemos" virou "olha o que dá para ver". E sobraram mais perguntas do que respostas, porque agora todo mundo quer saber por que aqueles números estavam escondidos.
Não vou entrar no debate sobre consumo de água de data center. Ele é importante, mas o que me interessa aqui é a falha técnica, porque ela é idêntica a uma coisa que eu vejo em code review com frequência.
Por que uma tarja preta falha
Num PDF, desenhar um retângulo preto em cima de um texto não apaga o texto. São duas camadas: o conteúdo original e um desenho por cima. O leitor de PDF mostra a tarja, mas o texto continua no arquivo. Você seleciona, copia, cola no bloco de notas e pronto.
Redação de verdade remove o conteúdo do arquivo. Ferramentas sérias fazem isso e ainda limpam metadados, histórico de revisões e camadas ocultas. Quem só desenha o quadrado está fazendo decoração.
Esconder na tela não é o mesmo que não enviar.
Guarde essa frase. Ela vale para PDF e vale muito mais para o seu frontend.
Dados sensíveis no React: a tarja preta do frontend
No React, a "tarja preta" tem várias formas. A mais clássica é esta:
function UserCard({ user }: { user: User }) {
return (
<div>
<h2>{user.name}</h2>
{user.role === 'admin' && <p>CPF: {user.cpf}</p>}
</div>
)
}
Você olha e pensa: "o CPF só aparece para admin". Na tela, é verdade. Mas o objeto user inteiro chegou no navegador. Abre o React DevTools, clica no componente, e o CPF está lá nas props. Ou abre a aba Network e lê o JSON da API.
Outras versões do mesmo erro que eu já encontrei por aí:
display: noneouhiddenem um bloco com dados que o usuário não deveria ver.- Um endpoint
/api/users/:idque devolve o registro inteiro do banco, incluindopassword_hash, e o frontend só usa três campos. - Feature flag escondendo um botão, mas a rota da ação continua aberta no backend.
- Variável de ambiente com prefixo
NEXT_PUBLIC_ouVITE_guardando uma chave que deveria ser secreta.
Em todos esses casos, a interface é a tarja. O dado está no arquivo, quer dizer, no bundle, no payload, no HTML.
O caso que pega até dev experiente: Server Components
Com React Server Components, o problema ficou mais sutil. Muita gente acha que tudo que roda no servidor fica no servidor. Fica, até você passar para um 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 tem 'use client'
}
Tudo que vai como prop para um componente com 'use client' é serializado e enviado para o navegador. O objeto inteiro. Se user tem password_hash, stripe_customer_id e internal_notes, isso tudo está no payload do RSC. Dá para ver no HTML da página, nos chunks do flight. No Pages Router era a mesma coisa com getServerSideProps: tudo ia parar no __NEXT_DATA__.
A correção é chata de tão simples. Selecione só o que a tela precisa:
const user = await db.user.findUnique({
where: { id },
select: { name: true, email: true, avatarUrl: true },
})
return <ProfileForm user={user} />
Se você usa Drizzle, Prisma ou SQL puro, a ideia é a mesma: o SELECT * é o retângulo preto desenhado por cima.
Como impedir o vazamento no seu projeto React
Não precisa de ferramenta mágica. Precisa de algumas regras que o time inteiro siga.
1. Monte um DTO na fronteira. Toda vez que um dado sai do servidor, ele passa por uma função que escolhe os campos. Pode ser um pick simples ou um schema do Zod com .parse(), que descarta o que não está declarado.
const PublicUser = z.object({
name: z.string(),
avatarUrl: z.string().url().nullable(),
})
return PublicUser.parse(user) // campos extras somem
2. Marque o que nunca pode ir para o cliente. O pacote server-only quebra o build se um módulo de servidor for importado num Client Component. E o React tem as APIs de taint (experimental_taintObjectReference e experimental_taintUniqueValue), que o Next.js expõe com a flag taint: true. Elas fazem o React jogar um erro se aquele objeto ou valor tentar atravessar para o cliente.
import { experimental_taintObjectReference } from 'react'
const user = await getUserFromDb(id)
experimental_taintObjectReference('Não passe o usuário cru para o cliente', user)
Não é bala de prata. Se você copiar o campo para outro objeto, o taint não acompanha. Mas pega o erro mais comum, que é passar o registro inteiro sem pensar.
3. Autorização mora no backend. Esconder o botão de "excluir" para quem não é admin é UX. Bloquear o DELETE no servidor é segurança. As duas coisas são necessárias, mas só uma delas protege alguma coisa.
4. Faça o teste do curioso. Antes de mandar uma feature para produção, abra a aba Network, veja o payload de cada request e pergunte: "eu ficaria tranquilo se isso aparecesse num print no X?". Leva dois minutos. É o equivalente a selecionar o texto embaixo da tarja e ver se ele cola.
O que essa história muda no seu dia a dia
O caso de Lincoln é um bom lembrete de que vazamento nem sempre vem de hacker sofisticado. Às vezes vem de alguém que confiou na camada visual. No PDF foi um retângulo. No React é um &&, um hidden, uma prop a mais.
Eu acho, e essa é a opinião forte do texto, que o ecossistema React facilitou demais esse erro nos últimos anos. A fronteira entre servidor e cliente ficou invisível de propósito, para ser ergonômica. Isso é ótimo para produtividade e péssimo para quem não sabe onde a linha passa. O 'use client' no topo do arquivo é, na prática, uma alfândega. Tudo que cruza ela está declarado para o mundo.
Então trate assim. Antes de passar uma prop de um Server Component para um Client Component, pergunte o que tem dentro dela. Antes de devolver um JSON, olhe os campos. E desconfie de qualquer dado que "só não aparece na tela".
O Google vai sobreviver ao constrangimento. Seu app talvez não sobreviva a um vazamento de CPF. Se quiser ver como eu organizo essa fronteira entre servidor e cliente nos meus projetos, dá uma olhada nos projetos que tenho publicado.
Resumo para o LinkedIn
Uma tarja preta mal aplicada expôs o que o Google queria esconder sobre o data center de Lincoln. O texto continuava no PDF, só tinha um retângulo desenhado por cima. No React a gente comete o mesmo erro todo dia: um `&&` que esconde o CPF na tela, mas manda o objeto inteiro para o navegador. Com Server Components ficou pior. Tudo que vai como prop para um `'use client'` vai junto no payload. Esconder na tela não é o mesmo que não enviar. DTO na fronteira, `select` só do necessário e autorização no backend. Escrevi sobre isso no blog, com código e um checklist rápido. Vale os 5 minutos antes do próximo deploy. #React #NextJS #SegurançaDaInformação #DesenvolvimentoWeb #FrontEnd