Repository navigation
Description
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:wpminteiro em faixa humana plausível (ex.0..350); acima disso, rejeita a linha ou a partida.accuracyinteiro0..100;errors>= 0.namestring não vazia,trim, comprimento<= 20(mesmo teto do cliente emuseRoom.tsjoin).placeinteiro>= 1quando presente.
- Tamanho de
resultslimitado aroom.max_players(12 no máximo absoluto); arrays maiores são rejeitados. - Uma partida com
wpmimplausível (ex. 99999) não aparece no leaderboard (node scripts/validate-persistence.mjscobre 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
caioross commentedon Jul 7, 2026
Resolvida em PR #8 (
auto/issue-7-sanitize-results).sanitizeResults(input, room)puro emsrc/lib/room.tscomo fronteira antes dopersistMatch: descarta linhas sem nick ou com WPM fora de0..350(o99999do vetor some do ranking), clampaaccuracy/errors/place/progress, trunca nick em 20 e limita o array aroom.max_players(teto 12). Cobri comscripts/validate-persistence.mjs(33 casos, verde). Caminho legítimo passa intacto.Gate:
typecheck✅ ·build✅ ·validate-persistence✅ 33/33.lintnã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.fix(anti-cheat): valida/clampa results antes de persistir no leaderbo…