fix(server): XSS, missing score submission, leaderboard never updated, no LIMIT
- leaderboard.html, replays.html: escape user-supplied display_name and username before inserting into innerHTML to prevent stored XSS - game.js: call POST /api/replays on win so browser-game completions are recorded; scores were never submitted before this fix - replays.rs: after replay insert, upsert leaderboard best_score / best_time_secs for opted-in users when the new score beats their current best (classic mode only); scores were never updated before this fix - leaderboard.rs: add LIMIT 100 to GET /api/leaderboard to prevent unbounded query growth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -320,6 +320,30 @@ function showWin(s) {
|
||||
const sec = elapsedSecs % 60;
|
||||
if (winTime) winTime.textContent = `${m}:${sec.toString().padStart(2, "0")}`;
|
||||
winOverlay.classList.remove("hidden");
|
||||
submitReplay(s);
|
||||
}
|
||||
|
||||
async function submitReplay(s) {
|
||||
const token = localStorage.getItem('fs_token');
|
||||
if (!token) return;
|
||||
const payload = {
|
||||
schema_version: 1,
|
||||
seed: Math.round(game.seed()),
|
||||
draw_mode: drawThree ? "draw_three" : "draw_one",
|
||||
mode: "classic",
|
||||
time_seconds: elapsedSecs,
|
||||
final_score: s.score,
|
||||
move_count: s.move_count,
|
||||
recorded_at: new Date().toISOString(),
|
||||
moves: [],
|
||||
};
|
||||
try {
|
||||
await fetch('/api/replays', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} catch (_) { /* best-effort — never block the win screen */ }
|
||||
}
|
||||
|
||||
// ── Auto-complete ─────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user