Meu amigo entrou no ranking sem jogar
Um amigo entrou no ranking dos meus jogos sem jogar. O desafio de proteger Pulse//Aim e Neon Snake me levou a reconstruir partidas e calcular resultados no servidor.
Como Pulse//Aim e Neon Snake nasceram de uma brincadeira com o ChatGPT e acabaram virando um desafio de segurança.
Na mesma noite em que a brincadeira começou, um amigo conseguiu aparecer na tabela de pontuação sem jogar.
Bastou enviar um POST.
Eu estava experimentando o recurso de criação de sites do ChatGPT e tinha criado dois jogos para uma competição entre amigos. A ideia era descobrir o que dava para fazer com aquela ferramenta, colocar os jogos para funcionar e deixar a disputa acontecer.
Só que alguém encontrou um caminho até o ranking que pulava a partida inteira.
Aquela entrada na tabela mudou o rumo da brincadeira. Eu propus um novo desafio: proteger meus jogos para que simplesmente enviar uma pontuação deixasse de ser suficiente.
Foi assim que Pulse//Aim e Neon Snake passaram a representar também uma experiência prática de segurança no servidor.
Dois jogos e um motivo para tentar de novo
O Pulse//Aim é um treino competitivo de mira. Cada sessão apresenta 30 alvos sequenciais, e o resultado combina precisão e rapidez: cada uma vale metade de uma pontuação que pode chegar a 10.000 pontos.
Isso cria uma disputa fácil de entender. Você precisa acertar, precisa ser rápido e precisa encontrar um equilíbrio entre as duas coisas. O jogo aceita mouse e toque, tem sons sintetizados e uma apresentação visual que acompanha o ritmo das partidas.
O Neon Snake Arena parte de uma ideia conhecida: o jogo da cobrinha. A partida acontece em um tabuleiro de 24 × 24, com controles por teclado ou gestos de toque. Conforme a pontuação cresce, a dificuldade também aumenta. O visual neon, os efeitos sonoros e o ranking global dão outra apresentação a uma mecânica familiar.
Os jogos são diferentes, mas compartilham um incentivo: fazer uma tentativa melhor e subir na tabela.
Quem já disputou uma posição com amigos sabe como um número pequeno consegue ganhar importância. Uma partida a mais passa a ter um motivo. Um recorde vira uma meta. A tabela mantém a brincadeira acontecendo depois da primeira rodada.
Foi nesse ambiente de experimentação e competição que o problema apareceu.
Um POST pulou a parte principal
Meu amigo enviou uma requisição POST e conseguiu entrar no ranking sem jogar.
POST é um método de comunicação usado na web para enviar dados a um servidor. Uma aplicação pode usá-lo ao receber um formulário, iniciar uma partida ou registrar um resultado.
Naquela noite, o envio direto conseguiu produzir uma entrada na tabela. O resultado estava lá, mas a partida que deveria dar sentido a ele não tinha acontecido.
Para a competição, a consequência era imediata. Quem jogava estava tentando melhorar dentro das regras. Outra pessoa tinha encontrado um caminho até a mesma classificação sem passar por elas.
A tentativa foi útil justamente por ser tão concreta. Havia uma entrada no ranking que mostrava o problema. Eu podia apontar para aquele comportamento e definir o que precisava mudar.
A pergunta passou a ser: o que o servidor precisa conferir antes de aceitar um resultado?
O servidor virou o árbitro
Propus proteger os dois jogos contra aquele tipo de envio direto.
O desafio me levou a olhar para o caminho entre a partida e a tabela. O navegador conduz a experiência, recebe os comandos do jogador e mostra o que está acontecendo. A decisão sobre o resultado que entra no ranking precisa de critérios próprios no servidor.
A solução implementada nos projetos usa uma ideia chamada replay determinístico.
O nome parece mais complicado do que a ideia. Uma partida começa com condições conhecidas. Se o servidor tiver essas condições e a sequência de ações enviada pelo navegador, ele consegue reproduzir os acontecimentos de acordo com as regras do jogo.
Uma dessas condições é a semente: um valor que permite gerar novamente a mesma sequência de elementos da partida. Com a mesma semente e as mesmas regras, o servidor consegue reconstruir os alvos ou a evolução da comida, por exemplo.
O navegador passa a enviar um registro de ações. O servidor usa esse registro para calcular o resultado.
Cada jogo aplica esse princípio de uma maneira.
No Pulse//Aim, os tiros precisam fazer sentido
No Pulse//Aim, a sessão começa no servidor, que define a semente usada para gerar os alvos.
Durante o jogo, o navegador registra os tiros. Ao final, envia coordenadas e marcas de tempo. O servidor reconstrói os alvos, percorre a sequência e verifica as colisões para determinar quais tiros acertaram.
Isso permite conferir se a sessão foi concluída e calcular a precisão a partir dos acertos e do total de tiros.
A rapidez também entra nessa conta. O tempo usado no ranking é calculado com timestamps do próprio servidor, descontando a contagem regressiva inicial. O relógio informado pelo navegador não define esse resultado.
Só depois dessas verificações o placar é calculado: até 5.000 pontos pela precisão e até 5.000 pela rapidez.
É uma mudança importante na responsabilidade de cada parte do sistema. O navegador registra o que está sendo enviado; o servidor verifica a sequência e produz a pontuação final.
Um número escolhido por alguém, sozinho, não contém as informações necessárias para passar por esse processo.
No Neon Snake, a pontuação nem vai no envio
O Neon Snake tem um detalhe que gosto especialmente: o placar não faz parte dos dados enviados pelo navegador.
O envio contém o apelido, a quantidade final de ticks e as mudanças de direção. Cada tick representa um passo da simulação do jogo.
Com a semente da sessão e esse histórico, o servidor refaz a trajetória da cobra. Ele recalcula onde ela esteve, quando encontrou comida, quanto cresceu, como a velocidade mudou e quando ocorreu a colisão que encerrou a partida.
Também verifica se a sequência respeita as regras. Uma reversão impossível ou um histórico que continua depois da colisão, por exemplo, não descreve uma partida válida para esse fluxo.
O tempo decorrido é comparado ao tempo mínimo calculado a partir da simulação, dentro da tolerância definida no código.
A pontuação nasce dessa reconstrução e é usada para atualizar o melhor resultado do apelido.
Esse desenho torna a proteção bem concreta: para chegar à tabela, o envio precisa representar uma trajetória que o servidor consiga reproduzir e aceitar.
Cada partida tem uma credencial própria
O replay faz parte de um fluxo de sessão.
Nos dois jogos, o servidor emite uma credencial de partida com validade limitada. Ela é entregue em um cookie com os atributos HttpOnly, Secure e SameSite=Strict, e o banco guarda apenas seu hash.
Esses atributos restringem a leitura pelo JavaScript da página e o envio do cookie em determinados contextos. O servidor ainda precisa verificar a credencial e os dados recebidos.
A credencial também tem uso único. Depois de um envio válido, ela é consumida, e uma nova submissão com a mesma sessão é rejeitada. Assim, o mesmo registro não pode ser aceito repetidamente por esse caminho.
Os projetos complementam o fluxo com verificações de origem, limites para o tamanho dos dados e a quantidade de ações, restrições de tentativas e consultas parametrizadas no banco.
São controles com responsabilidades distintas, trabalhando junto da reconstrução da partida.
O formato binário ajuda na comunicação
Os dois jogos usam um protocolo binário compacto e versionado para enviar os registros das partidas.
Esse formato reduz o volume de dados e torna a mensagem menos autoexplicativa em uma inspeção casual. As validações conferem a versão, o tamanho e a estrutura esperada antes de processar o conteúdo.
A proteção do placar depende do que o servidor faz com os dados: verificar a sessão, interpretar as ações, aplicar as regras e calcular o resultado.
O formato do protocolo continua sendo público. Trocar a apresentação dos dados não cria um segredo que o navegador possa guardar.
Esse foi outro aprendizado interessante do desafio. Cada parte da solução precisa ter uma função clara. Um formato mais compacto resolve uma necessidade de comunicação; a integridade do ranking exige verificações sobre a partida.
O mesmo atalho deixou de funcionar
Depois da proteção, já não conseguiam simplesmente mandar um POST e aparecer na tabela daquela maneira.
Esse foi o resultado observado no desafio. O caminho que havia funcionado no começo deixou de ser suficiente.
Os jogos continuam usando requisições POST em seu funcionamento normal. O que mudou foi o critério para aceitar um resultado: a submissão precisa estar ligada a uma sessão válida e passar pelas verificações implementadas no servidor.
Para quem estava participando da brincadeira, a diferença tinha um significado direto. A tabela precisava representar a competição que estávamos tentando criar.
Para mim, o episódio conectou uma situação de uso real a uma decisão de arquitetura. Eu tinha visto o problema acontecer, proposto um desafio para enfrentá-lo e acompanhado uma mudança no comportamento dos jogos.
Uma partida coerente ainda pode ser automatizada
O replay determinístico verifica se as ações descrevem uma partida coerente com as regras.
Uma sequência válida pode ter sido produzida por automação. Por isso, essa proteção tem um alcance específico: conferir a consistência do resultado enviado. Detectar bots e comprovar que uma pessoa realizou a partida exigem outras medidas.
Os próprios documentos dos jogos registram essa distinção.
Entender esse limite também fez parte do aprendizado. A tentativa simples que motivou o desafio foi enfrentada, e os projetos passaram a ter uma base melhor para discutir as próximas exigências de uma competição.
Criar com IA me deu um problema real para resolver
O ChatGPT participou da história desde a criação dos sites até as conversas sobre proteção.
Os projetos combinam React, Next.js e TypeScript, com execução por vinext e Vite, Cloudflare Workers e um banco D1 para os rankings. O áudio é gerado com Web Audio API; no Neon Snake, o tabuleiro é desenhado com Canvas 2D.
Essa base tornou possível experimentar duas ideias e colocar outras pessoas em contato com elas. A competição revelou um problema que deu mais direção ao trabalho.
As perguntas passaram a ter um alvo concreto. Por que aquele envio foi aceito? Como vincular um resultado à sessão? Que informações permitem reconstruir a partida? Qual parte do sistema deve calcular a pontuação?
Foi nessa passagem da ideia para o uso que a experiência com IA ficou mais interessante para mim. Eu tinha algo real para observar, discutir e melhorar.
Meu amigo queria encontrar um atalho até a tabela. Acabou me dando um motivo para estudar o caminho inteiro.