O Plyscope roda inteiramente no navegador de quem usa. Não há servidor, não há banco de dados, não há conta e não há nada nosso para invadir. Isso muda a forma da superfície de ataque, mas não a elimina: o app processa texto que vem de fora e o transforma em HTML na tela.
Este documento diz o que interessa relatar, para onde mandar e em quanto tempo você recebe resposta de verdade.
Não abra uma issue pública para uma falha explorável.
Duas vias, nesta ordem:
- GitHub Security Advisories — aba Security do repositório → Report a vulnerability. É privado, fica anexado ao repositório e permite discutir o patch antes de qualquer coisa aparecer.
- Contato direto do mantenedor — o perfil no GitHub, se preferir não usar a aba Security.
O que ajuda a resolver rápido, em ordem de utilidade:
- o PGN, o nome de usuário ou a resposta de API que reproduz — colado inteiro, mesmo que seja feio; é o insumo do bug;
- o caminho até ele: colou o PGN, arrastou o arquivo, buscou no Chess.com/ Lichess, reabriu uma análise salva;
- navegador e versão, sistema operacional;
- onde estava rodando:
plyscope.vercel.app, um dos atalhos locais (.bat/.command/plyscope.sh) ou outro servidor estático — importa porque o CSP e os cabeçalhos de isolamento mudam entre eles; - o que aconteceu de errado e o que deveria acontecer.
Prova de conceito é bem-vinda. Não precisa vir com correção.
| Versão | Coberta |
|---|---|
ponta da main / o que está em plyscope.vercel.app |
sim |
| última tag publicada | sim |
| qualquer coisa anterior | não |
Não há branch de manutenção. A correção sai na main, e a publicação é um
push: o site estático republica em menos de um minuto. Quem roda uma cópia
local atualiza com git pull — e, se editou src/, com python3 src/build.py.
A superfície real são duas entradas de dado que o app não controla:
1. O PGN. Colado, arrastado, escolhido no seletor ou vindo da API. Ele é texto arbitrário, e partes dele (nomes de jogadores, evento, site, data, resultado, comentários, cabeçalhos) vão parar na tela. Interessa:
- injeção de HTML ou de script por qualquer campo do PGN — inclusive dentro de
atributo (
title=,aria-label=,data-*=), que é onde um escape incompleto morde; - PGN que trave a página, estoure a memória ou faça o parser rodar sem fim (recursão, variantes aninhadas, milhares de lances);
- PGN que faça o app gravar em
localStoragealgo que, ao ser reaberto, execute — a análise salva é reidratada depois, e esse é o caminho mais fácil de esquecer.
2. A resposta das APIs públicas de api.chess.com e lichess.org. O app
consulta as duas direto do navegador, sem login. Um proxy hostil, um DNS
envenenado ou simplesmente um campo estranho na resposta são JSON não confiável
chegando pelo mesmo caminho do item 1. Interessa: qualquer campo dessa resposta
que vire HTML sem escape.
Também está no escopo:
- desvio da política de segurança de conteúdo — um jeito de executar script
no
index.htmlpublicado apesar dodefault-src 'none'e doscript-srcpor hash; - vazamento de dado para fora da máquina — qualquer requisição que o app
faça para uma origem que não seja
self,api.chess.comoulichess.org; ou o nome de usuário / o PGN aparecendo emReferer, em URL, emsessionStoragecompartilhado, onde não deveria; - os servidores locais (
servidor.ps1,tools/servidor.py,tools/servidor.js): eles servem a pasta do projeto emlocalhost:8123. Travessia de caminho para fora dessa pasta, ou exposição para fora delocalhost, é falha; - os cabeçalhos publicados (
vercel.json,_headers): política que não bate com oindex.htmldo commit — o que, na prática, quer dizer hash de script desatualizado.
- Vulnerabilidades do Stockfish. O motor é de terceiros
(nmrugg/stockfish.js, build do
Stockfish) e está versionado em
engine/. Reporte na origem; aqui só entra o que for específico de como o app o carrega ou o alimenta. - Ataques que exigem a máquina já comprometida ou que a pessoa cole código no console do navegador — o clássico self-XSS. Se o adversário já executa código no seu computador, o Plyscope é o menor dos problemas.
- Abrir o
index.htmlemfile://. Não é um modo suportado: o navegador nem instancia o.wasmali. Está explicado no README. - Um fork hospedado sem os cabeçalhos. O
vercel.jsone o_headersvêm prontos; quem publica em outro lugar e não os aplica está rodando outra configuração, não esta. - Falta de HTTPS, de HSTS ou de cabeçalho X-qualquer-coisa na Vercel — o
que o projeto controla está em
vercel.json, e otools/test-csp.jsconfere. - Rate limit, disponibilidade ou termos de uso das APIs do Chess.com e do Lichess. Não são nossas.
- Relatório de scanner automático sem prova de exploração. "Ferramenta X apontou Y" não é um relatório; a reprodução é.
Para você não gastar tempo redescobrindo o que está no lugar:
- Escape de HTML na saída. Todo dado que vem de fora passa por
esc()antes de virar HTML. Ele escapa<,>,&,"e'— as aspas entraram no commit8fafe63justamente porqueesc()também é usado dentro de atributo, onde escapar só os sinais de menor e maior não basta. Há teste com um nome de jogador hostil (um<img onerror>sem aspas, que atravessa o parser de PGN de verdade) emtools/test.js. - Content-Security-Policy por hash, fechada por padrão.
default-src 'none', ebase-uri,form-action,frame-ancestors,frame-srceobject-srctambém em'none'. Os<script>são inline e liberados um a um por SHA-256 dos bytes — não há'unsafe-inline'emscript-src, nem'unsafe-eval'; só'wasm-unsafe-eval', que o Stockfish exige.connect-srclista exatamente'self',https://api.chess.comehttps://lichess.org.worker-src 'self'(o motor sobe deengine/, nunca deblob:).img-src 'self' data:para o favicon e as setas do CSS. - A mesma política em produção e em desenvolvimento. Os três servidores
locais leem o CSP do
_headers, que é gerado pelosrc/build.pyjunto com oindex.html. Não existe uma política de "dev" mais frouxa que esconda um erro até o deploy. - A política não pode envelhecer em silêncio.
tools/test-csp.js(66 conferências) recalcula os hashes a partir doindex.html, extrai do código as origens defetch, o caminho do Worker e os usos dedata:, confere cada um contra as diretivas e sobe os servidores de verdade para checar por HTTP. Roda no CI. Um hash velho não degrada: ele bloqueia o app inteiro, e o teste cai antes. - Isolamento de origem cruzada.
Cross-Origin-Opener-Policy: same-origineCross-Origin-Embedder-Policy: require-corp, maisX-Content-Type-Options: nosniffeReferrer-Policy: strict-origin-when-cross-origin. - Nada sai da máquina. Não há telemetria, analytics, cookie, pixel, fonte externa nem CDN. O único tráfego de saída é a busca opcional de partidas nas duas APIs públicas, disparada por quem clica.
- O que fica guardado é
localStoragedo próprio navegador: até 20 análises, o idioma, o nome de usuário digitado na busca e a preferência de som. Sai apagando os dados do site.
Isto é um projeto pessoal, mantido por uma pessoa, sem empresa e sem plantão. Os prazos abaixo são o que dá para cumprir de verdade — não são um SLA:
| etapa | prazo |
|---|---|
| confirmar que li o seu relato | até 7 dias |
| dizer se é falha, e a gravidade | até 14 dias |
correção do que for explorável na main |
o mais rápido possível — costuma ser um arquivo e um push |
Se passar de 14 dias sem resposta nenhuma, assuma que a mensagem se perdeu e insista, por qualquer uma das duas vias.
Peço 90 dias de divulgação coordenada antes de tornar público, ou até a correção estar publicada, o que vier primeiro. Se você preferir prazo menor, diga no primeiro contato — é conversa, não imposição.
Não há programa de recompensa; não há dinheiro no projeto (ele aceita café, não
o contrário). O que há: crédito no CHANGELOG.md e no commit da correção, com
o nome ou o apelido que você escolher — ou nenhum, se preferir anonimato.