Skip to content

Segurança/Multiplayer: action start não exige status = lobby — um POST reinicia corrida em andamento (route.ts:147-164) #131

Closed

Description

@caioross

Contexto

A action start (src/app/api/rooms/[code]/route.ts:147-164) só verifica isLeader. Ela não
exige que a sala esteja em lobby
— o update é incondicional no status:

const res = await applyRoomUpdate(sb, code, "start", {
  status: "racing", snippet, start_at: startAt, results: null
});

Ou seja: start postado no meio de uma corrida em andamento sorteia um snippet novo, zera
results e joga start_at para Date.now() + COUNTDOWN_MS (4 s no futuro)
, para todo mundo que
está digitando.

Como reproduzir (anônimo, sem login)

  1. GET /api/rooms lista as salas públicas com o código (src/app/api/rooms/route.ts).
  2. GET /api/rooms/<code> faz select("*") e devolve leader_id em claro.
  3. Com a corrida rolando: POST /api/rooms/<code> {"action":"start","playerId":"<leader_id>"}.

O passo 3 nem precisa de sequestro quando a sala é sua; e a #6 (claim-leader sem autorização, já
decisao-dono) dá o passo 2 de graça em qualquer sala.

Por que é bug próprio, e não parte da #34

Independe de anti-cheat: hoje, na main, um start no meio da corrida já destrói a partida em
andamento de todos os jogadores da sala (snippet trocado embaixo dos dedos, results zerado). O
custo disso sobe se a #34 entrar com um filtro que descarta linha por tempo decorrido: start_at no
futuro faz o finish seguinte ver elapsed = 0 e apagar todos os resultados honestos, com a
sala flipando para finished com results: [] (achado das três lentes no quórum da PR #119, SHA
2247d44).

Acceptance criteria

  • start só transiciona a partir de lobby — update condicional (.eq("status", "lobby"), no
    mesmo padrão do finish, que já usa .eq("status", "racing")), não checagem lida antes.
  • start numa sala em racing/finished responde erro honesto (409) sem tocar snippet,
    start_at nem results.
  • Revanche continua funcionando: o caminho real do produto é reset (→ lobby) e só então
    start — confirmar no fluxo de useRoom/RoomView que nenhum caminho legítimo chama start
    direto de finished.
  • Teste em src/lib/room.test.ts (ou espelho em scripts/validate-persistence.mjs) cobrindo
    start fora de lobby.

Achado pelo PR Doctor durante o quórum da PR #119 (rodada de 2026-08-05).

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 Aug 5, 2026
  2. caioross commented on Aug 5, 2026

    @caioross
    OwnerAuthor

    Resolvida na PR #133 (non-draft, quórum §7.2).

    Como: start virou transição condicional lobby→racing — a guarda { status: "lobby" } vai na própria escrita (.match()), no mesmo padrão que o finish já usa com .eq("status","racing"), e não numa checagem lida antes (o SELECT da linha 106 deixaria o TOCTOU aberto). A decisão mora numa função pura nova, startRaceSpec (src/lib/room.ts), e roomUpdateOutcome ganhou um conflict? opcional para o 409 honesto — sem ele, o comportamento do #56 fica idêntico.

    AC 3 (revanche): confirmado no código — RoomView.tsx:94 só monta o <Lobby onStart> em status === "lobby"; de finished o único caminho é onPlayAgain → resetToLobby (reset → lobby) e só então start. Nenhum caminho legítimo chama start direto de finished.

    AC 4 (teste): 6 casos em src/lib/room.test.ts. Verificados contra mutação: trocando match: { status: "lobby" } por {}, o suite fica vermelho (1 failed / 198 passed).

    Gate: typecheck ✅ · vitest 199 passed ✅ · build ✅ · validate-persistence 78/78 ✅ · validate-metrics 45/45 ✅.

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