fix(server): XSS, missing score submission, leaderboard never updated, no LIMIT
Build and Deploy / build-and-push (push) Successful in 4m14s

- 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:
funman300
2026-05-13 19:32:14 -07:00
parent f0b9536e09
commit 09fcd2097e
7 changed files with 77 additions and 8 deletions
+2 -1
View File
@@ -54,7 +54,8 @@ pub async fn get_leaderboard(
CASE WHEN l.best_score IS NULL THEN 1 ELSE 0 END ASC,
l.best_score DESC,
CASE WHEN l.best_time_secs IS NULL THEN 1 ELSE 0 END ASC,
l.best_time_secs ASC"#
l.best_time_secs ASC
LIMIT 100"#
)
.fetch_all(&state.pool)
.await?;
+26
View File
@@ -114,6 +114,32 @@ pub async fn upload(
.execute(&state.pool)
.await?;
// Update leaderboard best score/time for opted-in users when this replay
// beats their existing best. Only classic mode counts for the leaderboard.
if header.mode == "classic" {
sqlx::query!(
r#"UPDATE leaderboard
SET best_score = ?,
best_time_secs = ?,
recorded_at = ?
WHERE user_id = ?
AND (
best_score IS NULL
OR ? > best_score
OR (? = best_score AND (best_time_secs IS NULL OR ? < best_time_secs))
)"#,
header.final_score,
header.time_seconds,
header.recorded_at,
user.user_id,
header.final_score,
header.final_score,
header.time_seconds,
)
.execute(&state.pool)
.await?;
}
Ok(Json(ReplayUploadResponse { id }))
}