3

Seu RG no servidor dos outros: por que conversores online são um risco e como processar arquivos no próprio navegador com WebAssembly

Quase todo mundo já passou por isso: o portal do concurso pede "foto 3x4 de até 50 KB", o RH pede "RG e comprovante num único PDF de no máximo 2 MB", e a solução mais rápida é jogar no Google "comprimir pdf online" e usar o primeiro resultado.

Neste post eu quero explicar o que acontece com o arquivo nesse fluxo, como você mesmo pode verificar isso, e como fazer o mesmo processamento sem que o arquivo saia da máquina. No fim falo do projeto que eu construí em cima disso. Aviso logo: eu sou o autor dele, então leve isso em conta.

O que acontece quando você "converte online"

A arquitetura típica de um conversor gratuito é simples:

  1. O navegador faz um POST multipart com o seu arquivo inteiro.
  2. O servidor grava o arquivo em disco ou num bucket (S3, GCS…).
  3. Um worker roda ffmpeg, ghostscript, libvips ou similar.
  4. O resultado fica disponível por um link durante "X horas".

Agora pense num PDF com RG frente e verso, CPF, data de nascimento e assinatura. Mesmo que o serviço seja honesto, essa cópia passa por:

  • Logs e backups. "Apagamos em 1 hora" costuma valer para o arquivo principal, não necessariamente para snapshots, filas de retry ou o log do proxy.
  • Terceiros. CDN, storage e ferramenta de monitoramento de erros (aquele stack trace que às vezes leva o nome do arquivo junto).
  • Jurisdição. Você raramente sabe em que país fica o servidor nem qual lei se aplica.
  • Links previsíveis. URLs de download sem autenticação, que vivem por horas.

Pela LGPD, CPF e imagem de documento são dados pessoais. Para quem desenvolve sistemas, isso quer dizer que mandar o usuário "converter no site X" antes de enviar para você cria um terceiro no fluxo que ninguém mapeou.

E tem um detalhe que pouca gente lembra: foto tirada com o celular carrega metadados EXIF, muitas vezes com coordenadas GPS. A foto do RG em cima da mesa de casa pode dizer onde fica a sua casa.

Como verificar se um site envia seu arquivo (30 segundos)

Não precisa confiar no "100% seguro" do rodapé de ninguém, inclusive no meu.

Teste 1: aba Network.

  1. Abra o DevTools (F12) → Network, marque Preserve log e limpe a lista.
  2. Processe um arquivo grande (uns 20 MB ajudam a enxergar).
  3. Ordene por tamanho e procure requests POST/PUT com corpo do tamanho do arquivo, ou vários pedaços (upload em chunks). A aba Payload mostra o que foi enviado.

Downloads (GET) de código, .wasm ou modelos são normais: é o site trazendo o processador até você, não levando o seu arquivo até ele.

Teste 2: modo avião. Abra a página, desligue a internet e só então processe o arquivo. Se funcionar, não teve upload. Se der erro de rede, teve.

Bônus para devs: olhe o header Content-Security-Policy. Dá para fazer o mesmo pelo terminal, pedindo só os cabeçalhos da página e procurando por connect-src. Uma diretiva connect-src restrita é o próprio navegador impedindo a página de mandar dados para hosts fora da lista.

Como processar localmente: três exemplos

O navegador moderno já tem quase tudo o que precisa. Onde não tem, o WebAssembly traz bibliotecas nativas compiladas.

1. Foto para no máximo N KB (só Canvas)

A ideia é uma busca binária na qualidade do JPEG. Se nem a qualidade mínima couber, você reduz a resolução e tenta de novo:

async function fitJpeg(bitmap, maxBytes) {
  const encode = (q, scale) => {
    const c = new OffscreenCanvas(Math.round(bitmap.width * scale), Math.round(bitmap.height * scale));
    c.getContext('2d').drawImage(bitmap, 0, 0, c.width, c.height);
    return c.convertToBlob({ type: 'image/jpeg', quality: q });
  };
  for (let scale = 1; scale > 0.05; scale *= 0.85) {
    let lo = 0.5, hi = 0.92, best = null;
    while (hi - lo > 0.02) {
      const mid = (lo + hi) / 2;
      const blob = await encode(mid, scale);
      if (blob.size <= maxBytes) { best = blob; lo = mid; } else hi = mid;
    }
    if (best) return best;
  }
  throw new Error('Não coube nem na menor resolução');
}

Um detalhe que aprendi na prática: portais discordam se 1 KB é 1000 ou 1024 bytes. Na dúvida, mire em limite × 1000 bytes, que passa nos dois critérios. Bônus: redesenhar no canvas já descarta o EXIF.

2. Juntar RG e comprovante num PDF (pdf-lib)

import { PDFDocument } from 'pdf-lib';

async function merge(files) {
  const out = await PDFDocument.create();
  for (const f of files) {
    const src = await PDFDocument.load(await f.arrayBuffer());
    const pages = await out.copyPages(src, src.getPageIndices());
    pages.forEach((p) => out.addPage(p));
  }
  return new Blob([await out.save()], { type: 'application/pdf' });
}

As páginas são copiadas como estão: o texto continua selecionável e nada é recomprimido. Para proteger ou remover senha, o qpdf compilado para WebAssembly resolve no cliente também.

3. Vídeo e formatos exóticos (ffmpeg.wasm)

O erro clássico com ffmpeg.wasm é usar writeFile, que copia o arquivo inteiro para a memória do wasm. Com um vídeo de alguns GB, a aba simplesmente morre. Montar o File como somente leitura via WORKERFS evita a cópia:

await ff.createDir('/in');
await ff.mount('WORKERFS', { files: [file] }, '/in');
await ff.exec(['-i', `/in/${file.name}`, '-vn', '-c:a', 'libmp3lame', 'out.mp3']);
const mp3 = await ff.readFile('out.mp3');
await ff.unmount('/in');

Para compressão de vídeo vale tentar antes a WebCodecs API, que usa o encoder de hardware da GPU e é muito mais rápida que o x264 rodando numa thread de wasm. O ffmpeg.wasm fica como fallback para MKV, AVI e companhia.

E para mascarar o CPF antes de mandar para alguém?

Regex sozinha gera muito falso positivo (qualquer sequência de 11 dígitos). Valide os dígitos verificadores:

function cpfValido(s) {
  const d = s.replace(/\D/g, '');
  if (d.length !== 11 || /^(\d)\1{10}$/.test(d)) return false;
  const n = [...d].map(Number);
  const dv = (len) => {
    let soma = 0;
    for (let i = 0; i < len; i++) soma += n[i] * (len + 1 - i);
    const r = (soma * 10) % 11;
    return r === 10 ? 0 : r;
  };
  return dv(9) === n[9] && dv(10) === n[10];
}

Limitações reais

Processar no cliente não é mágica:

  • Memória é o limite. Leia arquivos grandes em pedaços com Blob.slice(), nunca com arrayBuffer() no arquivo todo.
  • Celulares antigos são lentos com wasm. O ffmpeg.wasm single-thread é uma ordem de grandeza mais lento que o nativo.
  • O primeiro uso baixa código (o core do FFmpeg tem dezenas de MB). Fixe a versão e confira o SHA-256 do que vem de CDN.
  • Analytics continua sendo tráfego. Se o site tem Google Analytics, você vai ver requests na aba Network. A pergunta certa é o que vai neles, não se existem.

O projeto

Eu juntei tudo isso num site chamado Nibbo (nibbo.org). Repetindo: eu sou o autor. São 67 ferramentas que rodam no navegador, incluindo comprimir vídeo para um tamanho exato (WhatsApp, Discord, e-mail), foto para o KB que o portal pede, juntar/dividir/comprimir/assinar PDF, remover fundo, legenda a partir da fala e conversores como HEIC→JPG e MP4→MP3. Tem também uma ferramenta de ocultar dados pessoais que reconhece CPF e CNPJ pelo dígito verificador, com a mesma lógica da função acima. Arquivos de até 3 GB, sem cadastro, sem marca d'água, em 16 idiomas incluindo português, e as páginas já abertas funcionam offline.

Escrevi um guia mais curto, para mandar para quem não é dev, com os dois testes acima: Conversor online é seguro?

Se você souber de algum caso em que o processamento local quebra (formato estranho, navegador específico, PDF gerado por sistema do governo), me conta nos comentários. Esse tipo de arquivo é o que mais me ensina.

Carregando publicação patrocinada...