O jeito mais barato de proteger um dado é não coletar
Por que apaguei a coluna de IP em vez de aplicar hash, o que o User-Agent revela sozinho, e como um CDN externo vazava o IP de todo visitante da home.
Parte 2 da série sobre adequar meu site à LGPD. Na parte 1 listei nove problemas que encontrei auditando o próprio código. Este post é sobre os três mais baratos de resolver — e sobre por que “barato” aqui significa apagar código, não escrever.
O princípio que economiza trabalho
A LGPD tem um princípio chamado necessidade (art. 6º, III): o tratamento deve se limitar ao mínimo necessário para a finalidade. Normalmente ele é apresentado como uma obrigação chata.
Na prática, ele é a única exigência da lei que reduz trabalho. Dado que você não coleta:
- não precisa de base legal declarada
- não precisa de prazo de retenção
- não precisa entrar no registro de tratamento
- não precisa ser incluído na exportação de dados
- não precisa ser eliminado quando alguém sai
- não pode vazar
Todos os outros deveres da lei somam tarefas. Este subtrai.
Caso 1: o IP que ninguém lia
Meu log de requisições de IA gravava o endereço IP de quem chamava. A primeira ideia foi a óbvia: aplicar um hash e guardar o resultado. Parece um bom meio-termo — você mantém a capacidade de correlacionar sem guardar o valor real.
É uma má ideia, e vale explicar por quê.
Hash de IPv4 não é anonimização. O espaço de endereços IPv4 tem cerca de 4,3 bilhões de valores. Um computador comum calcula alguns bilhões de hashes por segundo. Ou seja: dado um hash, recuperar o IP original é questão de segundos de força bruta.
Você pode adicionar um segredo (salt) ao hash, o que impede a força bruta direta. Mas aí o dado continua sendo pseudonimizado, não anonimizado — a LGPD só desobriga do seu regime o dado verdadeiramente anônimo e irreversível (art. 12). Pseudonimizado continua sendo dado pessoal. Você teria de:
- documentar a finalidade do hash
- definir prazo de retenção
- ter política de rotação do salt
- incluir no registro de tratamento
- eliminar quando o titular pedir
Tudo isso para um campo que, na minha auditoria, tinha zero leitores. Nenhuma consulta, nenhum painel, nenhum relatório usava.
Então a decisão foi apagar a coluna.
O medo era: “e o controle de abuso?” Fui verificar. O limitador de requisições lia o IP direto do cabeçalho da requisição, em memória, sem nunca consultar a tabela. Continuou funcionando exatamente igual.
Fui além: como o valor não ia mais para lugar nenhum, removi também a linha que lia o cabeçalho. Hoje aquelas rotas nem chegam a ler o IP do visitante.
- const clientIp = request.headers.get('x-forwarded-for') || ...
Uma linha deletada em três arquivos, mais um DROP COLUMN. Foi a mudança de
maior impacto por caractere de todo o projeto.
Caso 2: o User-Agent que já estava resumido
A tabela de páginas vistas guardava a string completa do navegador. Algo assim:
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/141.0.0.0 Safari/537.36
Isoladamente, parece inofensivo. O problema é a combinação. Na mesma linha eu também guardava cidade, fuso horário, resolução de tela e tamanho da janela.
Junte tudo: alguém em uma cidade pequena, com uma resolução incomum, num navegador de versão específica, num fuso menos usual. Esse conjunto pode ser único. Isso tem nome — fingerprinting — e é o mecanismo que permite reconhecer alguém sem cookie nenhum.
Eu não estava fazendo isso. Mas guardar os ingredientes de um tratamento que você não faz é acumular risco sem contrapartida.
E aqui vem a parte boba: o código já extraía navegador, versão, sistema operacional e tipo de dispositivo dessa string. Os painéis usavam esses campos derivados. A string bruta ficava guardada ao lado, sem função.
A mudança foi deletar uma linha. A função que interpreta o User-Agent continua rodando — ela apenas recebe o valor, deriva o que interessa e descarta.
Esse é o padrão que vale generalizar: derive na entrada, guarde o derivado, descarte o bruto. Você fica com a informação analítica e sem o identificador.
Caso 3: o CDN que eu não tinha declarado
A página inicial tem uma animação 3D. O código carregava a biblioteca assim:
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
Isso significa que o navegador de todo visitante faz uma requisição para o jsDelivr. Nessa requisição vai o IP, o User-Agent e o endereço da página de origem.
Eu não coleto nada disso. Mas causo a entrega desses dados a um terceiro que o visitante não escolheu e que eu não declarei em lugar nenhum. É o mesmo raciocínio das decisões europeias sobre sites que carregam fontes do Google remotamente em vez de hospedar.
A correção foi quase cômica: a biblioteca já estava instalada no projeto. As simulações da área educacional a importavam normalmente. Só a home carregava de fora, por um resquício de quando aquela animação foi escrita.
import * as THREE from 'three';
Ganho colateral que eu não esperava: quem visitava a home e depois uma aula baixava duas cópias diferentes da mesma biblioteca — a versão do CDN e a do projeto. Agora há um único arquivo compartilhado. Meio megabyte a menos, e um subprocessador a menos na lista.
Aliás, ele agora aparece numa seção que criei na lista de subprocessadores chamada “Removidos”. Achei que valia registrar as saídas, não só as entradas.
Uma armadilha na hora de aplicar
Um detalhe operacional que quase me derrubou: a ordem entre código e banco.
Se você apaga a coluna no banco antes de publicar o código que parou de escrever nela, a versão em produção continua tentando inserir um campo que não existe. No meu caso, a função de telemetria captura erros e apenas registra no console — ela não propaga. Ou seja: a telemetria pararia de gravar em silêncio, e eu só descobriria semanas depois olhando um painel vazio.
A ordem certa é sempre: publicar o código → confirmar que está no ar → depois alterar o banco. Escrevi isso em letras garrafais no cabeçalho da migração, para o eu do futuro.
O que eu levo disso
Antes de perguntar “como eu protejo este dado?”, pergunte “eu preciso deste dado?”. Nos três casos aqui, a resposta era não, e a proteção virou uma linha deletada.
Um teste rápido que passei a fazer com qualquer campo pessoal: quem lê isto? Se a resposta for “ninguém, mas pode ser útil”, o campo não deveria existir. “Pode ser útil” é como se acumula passivo de dado pessoal sem perceber.
Na parte 3, a parte difícil: como apagar a conta de alguém sem apagar a nota fiscal que a lei obriga a guardar por cinco anos.