Skip to content

Segurança: finish aceita results não validados → leaderboard global forjável (anti-cheat) #7

Closed

Description

@caioross

Problema — envenenamento do leaderboard por results não validados (anti-cheat)

A ação finish em src/app/api/rooms/[code]/route.ts:67-78 aceita um array results totalmente fornecido pelo cliente e o persiste, via persistMatch (linhas 101-138), direto nas tabelas matches/scores — que alimentam o leaderboard global público:

case "finish": {
  const results: ResultRow[] = Array.isArray(body.results) ? body.results : [];
  // ... flip racing→finished ...
  if (flipped && flipped.length) await persistMatch(sb, room as RoomRow, results);
}

Não há: verificação de que o chamador participou da sala, limites de sanidade em wpm/accuracy/errors, limite de tamanho do array, nem de comprimento de name. O único saneamento é Math.round(x) || 0, que não rejeita valores absurdos (ex. wpm: 99999).

Vetor de ataque

Um atacante cria a própria sala, chama start, e então POST /api/rooms/<code> com { "action": "finish", "results": [{ "name": "qualquer_nick", "wpm": 99999, "accuracy": 100, "place": 1, "finished": true }] }. Isso injeta uma entrada arbitrária no topo do leaderboard global, com qualquer nome (inclusive personificando outro jogador). Repetível à vontade.

Impacto

  • O leaderboard — o principal artefato competitivo durável do produto — fica trivialmente forjável.
  • Personificação de nick (qualquer name) e valores impossíveis destroem a credibilidade do ranking.

Acceptance criteria (verificáveis)

  • Antes de persistir, o servidor valida e clampa cada linha de results:
    • wpm inteiro em faixa humana plausível (ex. 0..350); acima disso, rejeita a linha ou a partida.
    • accuracy inteiro 0..100; errors >= 0.
    • name string não vazia, trim, comprimento <= 20 (mesmo teto do cliente em useRoom.ts join).
    • place inteiro >= 1 quando presente.
  • Tamanho de results limitado a room.max_players (12 no máximo absoluto); arrays maiores são rejeitados.
  • Uma partida com wpm implausível (ex. 99999) não aparece no leaderboard (node scripts/validate-persistence.mjs cobre o caminho de persistência; adicionar caso que exercite os limites).
  • O caminho legítimo (resultados normais) continua persistindo igual — validação não altera dados válidos.

Dica de abordagem

Extrair um sanitizeResults(results, room) puro (fácil de testar) e chamá-lo no case "finish" antes do persistMatch. Reaproveitar os tetos de anti-cheat da engine quando existirem (ver skill cr-typing-engine) para manter uma única fonte de verdade do "WPM plausível". Verificação completa server-authoritative da corrida está fora de escopo (a engine é client-side); o objetivo aqui é limitar o raio de dano com validação de fronteira.

Escopo/autonomia

Área de quórum (HANDBOOK §7.2 — persistência + anti-cheat). Resolvível por agente com 3× APROVA no PR Doctor.

Activity

  1. added
    P1Alto valor — próximo da fila
    area:multiplayerSalas, Realtime, presença, leaderboard, persistência
    area:securityRLS, service_role, validação de entrada, segredos
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    and removed
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    on Jul 7, 2026
  2. caioross commented on Jul 7, 2026

    @caioross
    OwnerAuthor

    Resolvida em PR #8 (auto/issue-7-sanitize-results).

    sanitizeResults(input, room) puro em src/lib/room.ts como fronteira antes do persistMatch: descarta linhas sem nick ou com WPM fora de 0..350 (o 99999 do vetor some do ranking), clampa accuracy/errors/place/progress, trunca nick em 20 e limita o array a room.max_players (teto 12). Cobri com scripts/validate-persistence.mjs (33 casos, verde). Caminho legítimo passa intacto.

    Gate: typecheck ✅ · build ✅ · validate-persistence ✅ 33/33. lint não roda no repo (sem .eslintrc, pré-existente; CI é só typecheck+build). Área de quórum (§7.2) — pedido de quórum no corpo da PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Alto valor — próximo da filaarea:multiplayerSalas, Realtime, presença, leaderboard, persistênciaarea:securityRLS, service_role, validação de entrada, segredos

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions