Skip to content

Performance/Área sagrada: 4 dos 5 eventos do canal público não têm teto no receptor — flood de progress derruba o fps da corrida inteira (useRoom.ts:236-261) #135

Open

Description

@caioross

O problema — o projeto já aprendeu essa lição, e aplicou em 1 dos 5 eventos

O canal coderacer:room:<CODE> é público: qualquer cliente com a anon key (que é pública,
está no bundle) entra nele e emite qualquer evento. O código sabe disso — a doc do
scheduleResync (useRoom.ts:333-338) diz, com todas as letras:

"O gatilho vem de um canal público: sem teto, um cliente hostil transformaria um flood de
mensagens (barato) num flood de GETs à API vezes o número de membros da sala."

E resolve — para um evento. Levantando os cinco handlers em origin/main@eee4b62:

evento handler teto no receptor efeito por mensagem
room useRoom.ts:270-272 ✅ RESYNC_MIN_INTERVAL_MS = 800 ms (:33, :339-346) releitura coalescida
progress useRoom.ts:236-241 ❌ setProgress → invalida o memo de players (:422-448) → re-render de RoomView/Race/RaceTrack
chat useRoom.ts:243-247 ❌ setChat(prev => [...prev.slice(-80), m]) — copia um array de 80 por mensagem
ready useRoom.ts:250-254 ❌ setReadyMap → mesmo memo de players
vote useRoom.ts:257-261 ❌ setVotesMap → activeVotes + tallyVotes (:456-463)

PROGRESS_THROTTLE_MS (120 ms, :29, aplicado em :586) não protege nada disto: é o
throttle do emissor, no código do próprio cliente honesto. Um cliente hostil simplesmente
não roda esse if. Do lado de quem recebe não existe freio algum.

Por que isto é área sagrada (HANDBOOK §2)

O alvo é a corrida em andamento. Um participante da sala — ou qualquer um que saiba o código de
6 caracteres — emite progress em laço e cada mensagem recria o roster (players é um
useMemo sobre progress, com map + filter + sort, :422-448) e dispara o efeito de
maybeFinish (:524-526) em todos os clientes da sala ao mesmo tempo. O TypingCore é memo
com props estáveis e não re-renderiza (o Conselho verificou isso em 05/08), mas a reconciliação
de RoomView/Race/RaceTrack roda na mesma main thread da textarea — que é o que o §2 declara
intocável. É griefing de custo praticamente zero contra o fps de todos os adversários.

E não é só o caso hostil: chat sem teto significa que um jogador segurando Enter no chat já
degrada a corrida dos outros, sem nenhuma má-fé.

Isto não é coberto pela #104: aquela é
rate limit no POST /api/rooms (HTTP, server-side). Este tráfego não passa por rota nenhuma —
vai direto do cliente ao Realtime do Supabase e volta para os outros clientes. Nenhum limite de
servidor o alcança; a defesa tem de estar em quem recebe.

Acceptance criteria

  • Cada um dos quatro handlers sem teto (progress, chat, ready, vote) passa a ter um
    limite por emissor (payload.id) — de modo que um id que exceda o orçamento seja
    descartado sem que os outros jogadores sejam penalizados. Um teto global por sala não
    serve: um griefer calaria a sala inteira.
  • O caminho honesto não regride: com todos os jogadores emitindo no ritmo do
    PROGRESS_THROTTLE_MS (120 ms), nenhuma mensagem legítima é descartada — inclusive numa
    sala cheia (ABSOLUTE_MAX_PLAYERS = 30, room.ts:282).
  • A mensagem final de um jogador nunca é descartada: progress com finishedAt != null
    (:585) decide place e alimenta os results do finish. Perdê-la por throttle
    falsificaria a classificação — o remédio não pode virar um vetor novo.
  • A decisão é pura e testada em src/lib/room.ts (no molde de reduceRoomSync), com o
    handler só aplicando o veredito — o teste é o que prova a AC, e não dá para provar isto por
    screenshot (ver abaixo).
  • progress continua sem autoridade nenhuma: o servidor segue sendo a fronteira anti-cheat
    (sanitizeResults). Esta issue trata de custo, não de confiança no conteúdo.
  • pnpm typecheck + pnpm test + pnpm build + node scripts/validate-persistence.mjs
    verdes.

Verificação — não tente por screenshot

A frota já registrou duas vezes que no Browser pane headless o requestAnimationFrame
congela
: framer-motion trava e o visual mente. Prove pela lógica instrumentada — um teste
que alimente a função pura com uma rajada de N mensagens do mesmo id e afirme quantas passaram,
mais um caso com o tráfego honesto de 30 jogadores a 8,3 Hz afirmando que passaram todas.

Escopo e classificação

Verificado em origin/main@eee4b62. Sem duplicata (--search por flood/broadcast/rate
limit/realtime e por throttle/progress/chat/grief: zero resultados).

Activity

  1. added
    P2Prioridade normal
    area:multiplayerSalas, Realtime, presença, leaderboard, persistência
    area:securityRLS, service_role, validação de entrada, segredos
    on Aug 6, 2026
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

    P2Prioridade normalarea: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