[object Object]

← back to CelebritySignatures

yoloforever C2 hardening (Cody FIX-FIRST, all verified real): leaderboard endpoint locked down — game/diff whitelist (no arbitrary-key disk-fill), score capped 250 (blocks client-asserted 9999), per-IP rate limit 10/min (429), submission-id for deterministic rank + id stripped from public list. Verified: cheat→250, junk key→400, 11 posts→429 (TK-10181)

cb358f70b522f75e5751f167fb51463e92466c27 · 2026-08-03 15:49:06 -0700 · Steve Abrams

Files touched

Diff

commit cb358f70b522f75e5751f167fb51463e92466c27
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 15:49:06 2026 -0700

    yoloforever C2 hardening (Cody FIX-FIRST, all verified real): leaderboard endpoint locked down — game/diff whitelist (no arbitrary-key disk-fill), score capped 250 (blocks client-asserted 9999), per-IP rate limit 10/min (429), submission-id for deterministic rank + id stripped from public list. Verified: cheat→250, junk key→400, 11 posts→429 (TK-10181)
---
 server.js | 38 ++++++++++++++++++++++++++------------
 1 file changed, 26 insertions(+), 12 deletions(-)

diff --git a/server.js b/server.js
index 8b8fe41..0837706 100644
--- a/server.js
+++ b/server.js
@@ -45,6 +45,7 @@ function readBodyBig(req) {
 // way a file leaves the server is the tracked /api/signature-file endpoint,
 // which appends a commission entry to data/download-ledger.jsonl per download.
 const UPLOADS_DIR = join(DATA, 'uploads-private');
+const LB_HITS = new Map(); // per-IP leaderboard POST timestamps (rate limit)
 const DEFAULT_PRICE_USD = 20;      // download price (payments wired later — Steve-gated)
 const DEFAULT_COMMISSION_PCT = 50; // owner's cut per download
 async function adminToken() {
@@ -138,29 +139,42 @@ createServer(async (req, res) => {
     }
 
     // ===== GAME LEADERBOARD (public, no account needed) =====
-    // Top-20 per game+difficulty. No auth — a name is optional and sanitized.
-    // Reversible local JSON; nothing customer-facing beyond a score list.
+    // Top-20 per game+difficulty. HARDENED (Cody gate, yoloforever C2):
+    //  - game/diff must be from a fixed WHITELIST → no arbitrary-key disk-fill
+    //  - score capped at the real max any game can produce (250) → blocks 9999
+    //  - per-IP rate limit (10 POST/min) → no spam-flood of leaderboard.json
+    //  - each entry carries a submission id → deterministic rank (no name+score collision)
     if (path === '/api/leaderboard') {
       const board = await load('leaderboard.json', {});
+      const GAMES = new Set(['whose', 'art', 'match', 'early', 'century', 'lightning']);
+      const DIFFS = new Set(['icons', 'scholar', 'curator', 'deep']);
+      const MAX_SCORE = 250; // > any legit run (10-20 rounds, ~20/round incl. streak+bonus)
       if (M === 'GET') {
-        const key = (url.searchParams.get('game') || '') + '|' + (url.searchParams.get('diff') || '');
-        return sendJSON(res, 200, { ok: true, top: (board[key] || []).slice(0, 20) });
+        const g = String(url.searchParams.get('game') || ''), d = String(url.searchParams.get('diff') || '');
+        if (!GAMES.has(g) || !DIFFS.has(d)) return sendJSON(res, 400, { ok: false, error: 'unknown game/diff' });
+        return sendJSON(res, 200, { ok: true, top: (board[g + '|' + d] || []).slice(0, 20).map(({ id, ...e }) => e) });
       }
       if (M === 'POST') {
+        // per-IP rate limit
+        const ip = (req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.socket.remoteAddress || '?').split(',')[0].trim();
+        const now = Date.now();
+        LB_HITS.set(ip, (LB_HITS.get(ip) || []).filter(t => now - t < 60000));
+        if (LB_HITS.get(ip).length >= 10) return sendJSON(res, 429, { ok: false, error: 'slow down — try again in a minute' });
+        LB_HITS.get(ip).push(now);
         const b = await readBody(req);
-        const game = String(b.game || '').replace(/[^a-z]/gi, '').slice(0, 20);
-        const diff = String(b.diff || '').replace(/[^a-z]/gi, '').slice(0, 20);
-        const score = Math.max(0, Math.min(9999, parseInt(b.score, 10) || 0));
+        const game = String(b.game || ''), diff = String(b.diff || '');
+        if (!GAMES.has(game) || !DIFFS.has(diff)) return sendJSON(res, 400, { ok: false, error: 'unknown game/diff' });
+        const score = Math.max(0, Math.min(MAX_SCORE, parseInt(b.score, 10) || 0));
         const name = (String(b.name || 'Anonymous').replace(/[<>&"]/g, '').trim() || 'Anonymous').slice(0, 24);
-        if (!game || !diff) return sendJSON(res, 400, { ok: false, error: 'game + diff required' });
+        const id = randomBytes(6).toString('hex');
         const key = game + '|' + diff;
         board[key] = board[key] || [];
-        board[key].push({ name, score, at: new Date().toISOString() });
+        board[key].push({ id, name, score, at: new Date().toISOString() });
         board[key].sort((a, b2) => b2.score - a.score);
-        board[key] = board[key].slice(0, 50); // keep a little headroom, serve 20
+        board[key] = board[key].slice(0, 50);
         await store('leaderboard.json', board);
-        const rank = board[key].findIndex(e => e.name === name && e.score === score) + 1;
-        return sendJSON(res, 200, { ok: true, rank, top: board[key].slice(0, 20) });
+        const rank = board[key].findIndex(e => e.id === id) + 1;
+        return sendJSON(res, 200, { ok: true, rank, id, top: board[key].slice(0, 20).map(({ id: _i, ...e }) => e) });
       }
     }
 

← 79cdb51 yoloforever C2: games social layer — server /api/leaderboard  ·  back to CelebritySignatures  ·  yoloforever C3: social-share polish — OG + Twitter summary_l d017b3d →