Skip to content

Anti-cheat/Engine: arrastar-e-soltar o snippet-alvo na textarea burla o anti-paste — corrida 100% sem digitar (CodeEditor.tsx:305-327) #61

Closed

Description

@caioross

O editor da corrida bloqueia paste (onPaste → preventDefault, TypingCore.tsx:noPaste) e a barra de status promete isso em voz alta: "paste off · backspace não pontua" (CodeEditor.tsx:341). Mas a textarea não trata drop — e o snippet-alvo está renderizado, selecionável, ao lado, no mesmo viewport.

O vetor (1 gesto de mouse, sem devtools)

  1. CodeDisplay desenha o alvo como texto normal em <span>s (CodeDisplay.tsx:74-92) — sem user-select: none, e não há regra global de seleção (globals.css não tem user-select/drag).
  2. O jogador seleciona o código-alvo com o mouse e arrasta para dentro da textarea (CodeEditor.tsx:305-327).
  3. O browser insere o texto solto e dispara input/change — onPaste nunca roda, porque drop é outro evento.
  4. handleInput (TypingCore.tsx:104-127) vê next.length > typed.length, compara char-a-char contra code, não acha nenhuma divergência: soma N acertos, 0 erros, e computeProgress vai a 100% de uma vez. Resultado: precisão 100%, WPM proporcional ao tempo decorrido — e no multiplayer o progresso vai por broadcastProgress como se tivesse sido digitado.

Não é o mesmo buraco da #34 (coerência temporal do finish no servidor) nem da #28 (teto de plausibilidade no leaderboard): essas são a rede de segurança server-side/leaderboard, e ambas ainda deixam passar o caso "arrastou e depois esperou" — dentro do plausível temporal, com 100% de precisão. Aqui o furo é na fronteira de entrada da engine, a mesma que o onPaste já defende. Fairness na sala com amigos (o coração do produto) não tem outra defesa.

Acceptance criteria

  • drop na textarea da corrida não altera value: onDrop e onDragOver com preventDefault(), em paridade com o onPaste já existente (CodeEditor.tsx:313).
  • Repro manual documentada no PR (antes/depois): selecionar o alvo no CodeDisplay, arrastar para o editor, soltar → texto não entra, progresso/precisão/WPM não se mexem.
  • Defesa em profundidade na fonte: arrastar a partir do CodeDisplay não inicia um drag útil (ex.: onDragStart → preventDefault no container). Não remover a seleção do texto (user-select: none prejudica acessibilidade e cópia legítima fora da corrida).
  • A promessa da barra de status continua verdadeira — se a copy mudar, que descreva o que de fato é bloqueado.
  • Área sagrada intacta: nenhum trabalho novo por keystroke; os handlers adicionados só rodam em drop/dragover. node scripts/validate-metrics.mjs verde.
  • Nenhuma fórmula de src/lib/metrics.ts alterada (a engine não muda; só a fronteira de entrada).

Dica de abordagem

Menor raio possível: a textarea já recebe onPaste vindo por prop de TypingCore (noPaste). Dá para tratar drop/dragover no mesmo lugar (bloqueio local no CodeEditor, já que não há caso de uso legítimo de soltar texto no editor da corrida) ou estender a prop para um onBlockedInsert único e reusar noPaste. Preferir o que deixar um ponto de verdade sobre "o que é entrada ilegítima".

Vale um comentário curto no código explicando por que drop precisa de tratamento próprio — foi exatamente o que passou despercebido quando o onPaste foi escrito.

Activity

  1. added
    P2Prioridade normal
    area:engineMotor de digitação, WPM, precisão, anti-cheat
    area:securityRLS, service_role, validação de entrada, segredos
    on Jul 24, 2026
  2. caioross commented on Jul 24, 2026

    @caioross
    OwnerAuthor

    🏛️ Parecer do Conselho · 2026-07-24

    Veredito: APROVADA PARA EXECUÇÃO — com um corte de escopo explícito (ver Engenharia).

    Revisado contra origin/main (d0c76ef), não contra a working tree. Nº 1 do Top-3 do Curador
    (Diário #4). Diagnóstico da issue confere:
    CodeEditor.tsx:305-327 monta a textarea com onPaste (:313), onScroll, onKeyDown,
    onKeyUp, onClick — e nenhum onDrop/onDragOver. Um drop nativo insere o texto e
    dispara input, que cai no onChange de :308-312 → handleInput (TypingCore.tsx:93-118),
    onde o loop de :106-108 compara char-a-char contra code e não acha divergência: 0 erros,
    computeProgress a 100%. O vetor é real e não precisa de devtools.

    Plano sugerido:

    1. Branch auto/issue-61-* a partir de origin/main. Fluxo §7.3 normal — não é área de quórum
      (src/components/**, sem tocar api/rooms/** nem persistência).
    2. CodeEditor.tsx:305-327 — adicionar na textarea, junto de onPaste:
      onDragOver={e => e.preventDefault()} e onDrop={e => e.preventDefault()}. Comentário curto de
      2 linhas dizendo por que drop precisa de tratamento próprio (não é paste) — foi
      exatamente o que passou despercebido; o comentário é o que impede a terceira ocorrência.
    3. Opcional e barato: onDragStart={e => e.preventDefault()} no container de CodeDisplay.tsx:57.
      Se custar mais que 2 linhas, deixe de fora e diga no PR — não é a defesa.
    4. Não mexer em TypingCore.tsx:93-118, em @/lib/metrics nem em @/lib/indent.
    5. Testes: não adicione jsdom/RTL. O runner é environment: "node" com
      include: ["src/**/*.test.ts"] (vitest.config.ts) — não há como testar handler de DOM hoje, e
      a dependência nova cairia em §7.2 por um fix de 4 linhas. A evidência é a repro manual
      antes/depois no corpo do PR, como a AC já exige.
    6. Gate: pnpm typecheck && pnpm build, pnpm test, node scripts/validate-metrics.mjs e
      node scripts/validate-persistence.mjs. Todos devem ficar idênticos — se algum número mudar,
      o diff saiu do escopo.
  3. added
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    and removed
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    on Jul 26, 2026
  4. caioross commented on Jul 26, 2026

    @caioross
    OwnerAuthor

    Resolvida em #94 (non-draft, quórum solicitado).

    Fix no destino: onDragOver + onDrop com preventDefault() na textarea
    (CodeEditor.tsx, ao lado do onPaste) — fecha a classe inteira de origens, inclusive drag de
    fora do browser, e cobre /practice de graça. Na fonte, onDragStart no container do
    CodeDisplay como cortesia, sem user-select: none. +9 linhas em 2 arquivos.

    Evidência: em produção hoje, dragover/drop na textarea saem com defaultPrevented: false
    (vetor confirmado); no bundle deste branch os dois handlers cancelam. Drag nativo humano não é
    reproduzível em headless — declarado no PR, verificação final no preview da Vercel.

    Gate verde: typecheck · build · 54 testes vitest · validate-metrics 37/37 ·
    validate-persistence 72/72 (lint N/A, sem config ESLint). Segui o plano do Conselho, divergindo
    só na classificação: pedi quórum porque §7.2 lista "anti-cheat" sem qualificar caminho.

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:engineMotor de digitação, WPM, precisão, anti-cheatarea: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