← 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 →