pt//
Web SecurityAppsecAPI SecurityBypass

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.