Maratona de segurança — 17 vulnerabilidades fechadas em duas sessões
🔒 Por que auditar
No último commit da sprint de lançamento, marquei um TODO: "rodar auditoria de segurança antes da próxima feature". Eu sabia que tinha pressa demais nos últimos 4 dias e que alguns handlers tinham passado sem peer-review.
A auditoria rodou na manhã do 29/05 e veio com 5 CRITICAL/HIGH:
- Handler
posaceitavahp/maxHpdo cliente - Sem
uncaughtExceptionglobal → throw emtickAImatava o processo ws.on('message')sem try/catch geral → mensagem ruim derrubava o serverpvpAttacksem cap emamount/range→ F12 one-shotpvpAttacksem rate limit → 100 hits/s + farm XP
#1 — O handler pos aceitava HP do cliente
Esse foi o mais embaraçoso. O lockdown N3 cobria saveUpload, playerSync e ~15 handlers de mutação. Mas o handler pos (que atualiza posição quando o player anda) estava assim:
case 'pos': {
p.x = clamp(msg.x, 0, MAP_W - 1);
p.y = clamp(msg.y, 0, MAP_H - 1);
if (typeof msg.hp === 'number') p.hp = msg.hp; // 🔴
if (typeof msg.maxHp === 'number') p.maxHp = msg.maxHp; // 🔴
break;
}
Esse if (typeof msg.hp === 'number') sobrevivia desde antes do lockdown — provavelmente porque "o cliente precisa atualizar HP quando regenera". Acontece que a regen tinha virado server-side desde o T2.
F12 no cliente, mandar {t:'pos', x:.., y:.., hp: 99999, maxHp: 99999} → players imortais.
Fix: remover qualquer mutação de stats do handler pos. Server é dono.
#2 e #3 — Sem uncaughtException, qualquer throw mata prod
Caso clássico de Node: tickAI roda a cada 100ms. Se algum mob entrar num estado inválido (item null, target já desconectado, etc.) e o handler dispara TypeError, o evento sobe até o topo do loop e mata o processo.
Railway reinicia, mas no meio:
- Players perdem conexão
- Estado em memória que não tinha persistido ainda some
- Logs de erro são parciais (nem sempre dá tempo de flush)
Fix duplo:
process.on('uncaughtException', (err) => {
console.error('[fatal:uncaught]', err);
// não rethrow — log e segue
});
process.on('unhandledRejection', (err) => {
console.error('[fatal:unhandled]', err);
});
E o handler de message:
ws.on('message', async (raw) => {
try {
const msg = JSON.parse(raw);
await dispatch(ws, msg);
} catch (err) {
console.error('[ws:msg]', err);
// não fecha o socket — só loga
}
});
Isso não é "fingir que tá tudo bem". É garantir que um player mal-intencionado não derruba o server pros outros.
#4 e #5 — pvpAttack sem cap nem rate limit
Esse handler aceita um amount calculado no cliente (legado pré-lockdown). F12, mandar {t:'pvpAttack', target:'X', amount: 99999} → one-shot.
Pior: nada limita frequência. Loop infinito mandando 100 attacks/segundo = farm de XP de skill via PvP.
Fix:
// Cap server-side por skill+equip
const maxAtk = computeMaxDamage(p);
const amount = Math.min(msg.amount | 0, maxAtk);
// Rate limit por conn
if (now - p._lastPvpAttack < 600) return; // 600ms entre hits
p._lastPvpAttack = now;
🟡 P0.5 — Mais 5 issues que apareceram
Depois de fechar os P0, fiz uma segunda passada e achei:
permaBuffsaceitava qualquer chave do cliente → construí allowlist a partir deTALENT_DEFSpkDeathconfiava emmsg.killerId→ server determina autonomamenteflags/questFlagssem allowlist → set de 6 keys válidas + validação porchainId- ADMIN_TOKEN passava por query string → migrei pra header
X-Admin-Token broadcastMobssempre full snapshot → skip-when-unchanged via signature + snapshot full só a cada 10s
🟡 Nova auditoria 29/05 à tarde — 6 vetores de rate limit
Bateu o sininho de "talvez ainda tenha gente abusando" e fiz uma terceira passada focada em rate limit:
- 🔴
announcesem admin check — qualquer player podia broadcast spam. Fix:isAdmin()+ 2s rate - 🟡
authsem rate limit — brute force passwords. Fix: 5 tentativas/30s por conn → fecha - 🟡
duelInvitesem rate limit — pop-up infinito de assédio. Fix: 3s - 🟡
tradeRequestsem rate limit — mesma coisa. Fix: 3s - 🟡
getRankingsem rate limit — CPU spike. Fix: 1s - 🟡
passwordResetRequestsem rate limit — CPU viafindAccountByEmailO(N). Fix: 5/min
📊 Resultado final
| Categoria | Quantos | |---|---| | CRITICAL (P0) | 5 | | P0.5 (HIGH) | 5 + 1 missed (pkDeath) | | Nova auditoria (rate limit) | 6 | | Total fechado em 2 sessões | 17 |
Commits:
🧠 Lição
Auditoria periódica não é luxo. É a única coisa que pega o canto cego do "eu já cobri isso no lockdown N3". O lockdown cobria saveUpload e playerSync. Não cobria pos. Não cobria pvpAttack. Não cobria announce.
Cada handler novo é um vetor novo. Trate como tal.
Próximo post: o overhaul mobile que veio porque a esposa achou ruim no celular.