Never Trust the Client: Como Consegui Manipular uma Aplicação com Sistema Antifraude
Uma análise detalhada sobre como burlei verificações antifraude no lado do cliente, manipulei requisições e por que validações no servidor são indispensáveis.
Recentemente, durante alguns testes em um daqueles sites onde duas pessoas aparecem na câmera em uma partida rápida e recebem uma pontuação, encontrei uma falha que acabou sendo bem mais interessante do que parecia no começo.
O funcionamento é simples: duas pessoas entram na partida usando a webcam e, durante alguns segundos, o próprio navegador analisa informações da câmera e calcula a pontuação de cada participante. A aplicação também possui mecanismos para tentar identificar fraudes, como detecção de imagem estática, análise de movimento e outras verificações de integridade.
Mesmo com essas proteções, havia um problema estrutural no design da aplicação: o navegador calculava a pontuação e depois dizia ao servidor qual havia sido o resultado final.
Foi a partir daí que comecei a testar até onde eu conseguiria controlar essa informação.
Nota: Os testes descritos neste artigo foram realizados exclusivamente para fins de pesquisa e aprendizado. Alguns detalhes foram simplificados e o código de exploração não será disponibilizado. O objetivo é discutir a falha do ponto de vista de segurança e arquitetura de software.
1. Encontrando a Pontuação na Requisição
Analisando o tráfego da aplicação pelo Burp Suite, identifiquei a requisição enviada ao final de cada partida:
POST /api/match/finalize HTTP/2
Host: target-app.com
Content-Type: application/json
{
"selfScore": 7.4,
"opponentScore": 6.8,
"selfScanValidity": {
"microMotionScore": "0.82",
"staticImageSuspicion": "low",
"sampledFrames": 120,
"landmarkStability": "high"
}
}
O campo selfScore chamou atenção imediatamente. Se aquele valor representava minha pontuação e estava sendo enviado diretamente pelo navegador, a pergunta era inevitável:
O que aconteceria se eu simplesmente alterasse esse valor antes de ele chegar ao servidor?
2. A Primeira Tentativa: Latência de Intercepção
Comecei pelo caminho mais óbvio: interceptei a requisição no Burp Suite e alterei manualmente o selfScore.
O problema é que as partidas acontecem em tempo real. Enquanto eu segurava a requisição no Burp para editar o JSON, o outro participante já havia enviado o resultado dele.
Minha Requisição ---> [ Parada no Burp para edição ]
Requisição do Oponente ---> [ Chega primeiro ao servidor ]
↓
Partida Finalizada
[ Minha Requisição Chega ] ---> Retorno: Partida já encerrada
Quando minha requisição finalmente chegava, era tarde demais. O problema não estava na alteração em si, mas no tempo que eu levava para fazê-la. Eu precisava automatizar a alteração diretamente no cliente.
3. Sobrescrevendo Funções no Console do Navegador
A solução foi mover o teste para o runtime do próprio navegador. Em vez de esperar a requisição chegar ao Burp para modificá-la, criei um script que interceptava a finalização antes do envio.
Sobrescrevi temporariamente o comportamento do window.fetch:
JavaScript
const originalFetch = window.fetch;
window.fetch = function (...args) {
const [url, config] = args;
if (typeof url === 'string' && url.includes('/api/match/finalize')) {
let body = JSON.parse(config.body);
body.selfScore = 9.8; // Força uma nota alta
config.body = JSON.stringify(body);
}
return originalFetch.apply(this, args);
};
Partida Termina -> Prepara Dados -> Hook Intercepta -> Altera Nota -> Requisição Enviada -> Servidor
Com o hook ativo, não existia mais o atraso da edição manual. A requisição modificada chegou instantaneamente ao servidor.
Porém, o resultado não foi o esperado. Coloquei uma pontuação próxima de 10.0, mas minha nota final virou:
1.0
4. Entendendo as Regras do Antifraude
A nota 1.0 não era um erro genérico: era o sistema antifraude reajustando a pontuação ao identificar dados inconsistentes.
Nos primeiros testes com o script, minha webcam estava praticamente sem movimento. Analisando os campos de selfScanValidity:
staticImageSuspicion: Muito Alto
microMotionScore: Praticamente Zero
Eu estava enviando um pacote que dizia:
Pontuação alegada: 9.8
Movimento na câmera: Zero
Suspeita de imagem estática: Alta
Para o sistema, aquilo indicava fraude clara e punia a pontuação para 1.0.
Ajustando a Validação da Câmera
Refiz o teste aparecendo normalmente diante da câmera, garantindo movimento orgânico, piscadas e variações normais de iluminação. Os dados de selfScanValidity passaram a ser legítimos.
Mesmo assim, forçar valores muito altos como 9.8 ou 9.9 ainda fazia a pontuação cair para 1.0.
5. Capturando Outros Transportes (navigator.sendBeacon)
Durante os testes, percebi que algumas finalizações não eram capturadas pelo meu hook no fetch().
A aplicação utilizava navigator.sendBeacon() como fallback para garantir o envio de dados caso o usuário fechasse a aba ou saísse da página.
Adaptei o script para cobrir também essa API:
JavaScript
const originalBeacon = navigator.sendBeacon;
navigator.sendBeacon = function (url, data) {
if (url.includes('/api/match/finalize')) {
// Intercepta e altera os dados do beacon
}
return originalBeacon.apply(this, arguments);
};
Agora todas as vias de envio estavam sob controle.
6. Manipulação Relativa vs. Valores Absolutos
Por que dados reais de câmera com notas altas ainda eram rejeitados?
O servidor também comparava os dados enviados pelos dois participantes. Se um usuário envia um resultado com uma diferença absurda (9.9 vs 5.2), mas a telemetria do oponente discorda do resultado, o sistema trata o payload como anomalia.
Em vez de forçar valores fixos fora da realidade, mudei para uma estratégia de manipulação relativa.
O próprio JSON continha o opponentScore calculated localmente. Ajustei a lógica para definir minha pontuação com base no valor do oponente:
JavaScript
// Em vez de: body.selfScore = 9.9
// Ajuste relativo:
body.selfScore = body.opponentScore + 0.8;
Pontuação Oponente: 6.2 ---> Minha pontuação alterada para: 7.0
Pontuação Oponente: 7.1 ---> Minha pontuação alterada para: 7.9
Mantendo os dados autênticos da câmera e aplicando apenas essa pequena diferença relativa, o servidor passou a aceitar o resultado como legítimo.
7. A Vulnerabilidade Central
A parte mais interessante é que não foi preciso quebrar os modelos de visão computacional da câmera nem reverter cada regra do antifraude.
O fluxo de execução continuou intacto:
Pessoa real diante da câmera.
Aplicação analisa a partida normalmente.
Telemetria real da câmera (selfScanValidity) é mantida.
Script ajusta apenas o selfScore em relação ao opponentScore.
Servidor aceita e processa o resultado.
O problema central foi permitir que o cliente determinasse o estado final de uma regra de negócio crítica.
[ Cliente Não Confiável ]
└── Processa Métricas e Calcula Pontuação
└── Envia Resultado Final ---> [ Servidor ] (Confia no dado recebido)
Não importa o quão avançada seja a validação no frontend: qualquer código executado no dispositivo do usuário pode ser alterado.
8. Como Corrigir?
A solução para esse tipo de falha não é ofuscar o código JavaScript, bloquear o DevTools ou tentar esconder os endpoints.
Abordagem Segura:
Cliente apenas Coleta: O navegador deve limitar-se a enviar os dados brutos de telemetria da câmera (marcadore sensoriais, métricas de movimento).
Servidor Decide: O backend processa as métricas recebidas, aplica os algoritmos antifraude e calcula a pontuação final de forma independente.
Validação Cruzada: O servidor confirma os dados de ambos os participantes antes de atualizar qualquer pontuação ou ranking.
Navegador (Cliente)
│
└── Envia Apenas Dados Brutos / Telemetria
│
▼
Servidor Backend
├── Valida Métricas da Câmera
├── Calcula a Pontuação da Partida
└── Atualiza o Ranking no Banco de Dados
Conclusão
Vulnerabilidades nem sempre se manifestam como falhas clássicas de injeção ou estouro de memória. Muitas das falhas mais críticas surgem na própria arquitetura e premissas do sistema.
Ao projetar qualquer aplicação, a pergunta fundamental deve ser sempre:
Se o navegador está me dizendo qual é o resultado, o que impede o usuário de alterar essa informação antes de enviá-la?
Never trust the client.