From 109eeebfb133d6f0e52c3efdcfd18ed24572cbff Mon Sep 17 00:00:00 2001 From: Clippii Date: Sun, 28 Jun 2026 16:01:57 +0100 Subject: [PATCH] updates to api protections --- README.md | 11 ++++++----- example.env | 2 +- frontend/src/App.jsx | 5 ++++- server.cjs | 24 +++++++++++++++++++++--- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 680640d..ece99e2 100644 --- a/README.md +++ b/README.md @@ -117,17 +117,18 @@ PUBLIC_DATA_CACHE_STALE_MS=86400000 PUBLIC_DATA_PREWARM_INTERVAL_MS=300000 PUBLIC_DATA_COLD_TIMEOUT_MS=8000 VITE_STATIC_DATA=false -VITE_SITE_GATE=false +VITE_SITE_GATE=true API_RATE_LIMIT_WINDOW_MS=60000 API_RATE_LIMIT_MAX=120 SITE_SESSION_SECRET=long-random-shared-secret SITE_SESSION_TTL_SECONDS=43200 ``` -HTML responses set a signed, HttpOnly site-session cookie. `/api/*` and `/data/*` -requests must present that cookie and same-origin browser request metadata, so the -data is served to active site sessions instead of as an open public API. All PM2 -web instances must share the same `SITE_SESSION_SECRET`. +Successful Turnstile verification sets signed, HttpOnly Turnstile and site-session +cookies. `/api/*` and `/data/*` requests must present those cookies plus +same-origin browser request metadata, so the data is served to verified active +site sessions instead of as an open public API. All PM2 web instances must share +the same `SITE_SESSION_SECRET`. On startup, the web server preloads the critical public snapshots before signalling PM2 `ready`: team leaderboard, player leaderboard, home teams, and diff --git a/example.env b/example.env index 5e3eb21..619deef 100644 --- a/example.env +++ b/example.env @@ -64,5 +64,5 @@ DISCORD_INCLUDE_PATCH=true # TURNSTILE_SECRET_KEY is the server-only secret used to call the Siteverify endpoint. VITE_TURNSTILE_SITE_KEY= TURNSTILE_SECRET_KEY= -VITE_SITE_GATE=false +VITE_SITE_GATE=true VITE_STATIC_DATA=false diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f7330a4..645bf78 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -59,7 +59,10 @@ const liveRefreshMs = 15000 const siteVersion = import.meta.env.VITE_SITE_VERSION || '1.0.0' const turnstileSiteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY || '' -const siteGateEnabled = String(import.meta.env.VITE_SITE_GATE || 'false').toLowerCase() === 'true' +const siteGateSetting = import.meta.env.VITE_SITE_GATE +const siteGateEnabled = siteGateSetting == null + ? Boolean(turnstileSiteKey) + : String(siteGateSetting).toLowerCase() === 'true' const staticDataBase = (import.meta.env.VITE_STATIC_DATA_BASE || '/data').replace(/\/+$/, '') const staticDataEnabled = String(import.meta.env.VITE_STATIC_DATA || 'false').toLowerCase() === 'true' const missingStaticDataPaths = new Set() diff --git a/server.cjs b/server.cjs index 1673b92..88ed662 100644 --- a/server.cjs +++ b/server.cjs @@ -1281,8 +1281,19 @@ function isProtectedDataPath(req) { } } +function isSiteSessionBootstrapPath(req) { + if (!req.url) return false + try { + const pathname = new URL(req.url, `http://${req.headers.host || 'localhost'}`).pathname + return pathname === '/api/turnstile/verify' || pathname === '/api/turnstile/session' + } catch { + return req.url === '/api/turnstile/verify' || req.url === '/api/turnstile/session' + } +} + function requireSiteSession(req, res) { if (!isProtectedDataPath(req)) return true + if (isSiteSessionBootstrapPath(req)) return true if (!SITE_SESSION_HMAC_KEY) { sendJson(res, 503, { error: 'Site session signing is not configured' }) @@ -1299,6 +1310,11 @@ function requireSiteSession(req, res) { return false } + if (!isTurnstileSessionVerified(req)) { + sendJson(res, 403, { error: 'Turnstile session required' }) + return false + } + return true } @@ -2469,12 +2485,10 @@ function htmlWithSeo(req, data) { function sendHtml(req, res, data, status = 200) { const html = htmlWithSeo(req, data) - const siteSessionCookie = buildSiteSessionCookie(req) send(res, status, html, { ...securityHeaders(req, { html: true }), 'content-type': mimeTypes['.html'], 'cache-control': 'no-cache', - ...(siteSessionCookie ? { 'set-cookie': siteSessionCookie } : {}), }) } @@ -2978,7 +2992,11 @@ const server = http.createServer((req, res) => { sendJson(res, 403, { error: 'Turnstile verification failed', detail: verification.error }) return } - const headers = TURNSTILE_SECRET_KEY ? { 'set-cookie': buildTurnstileSessionCookie(req) } : {} + const cookies = [ + buildTurnstileSessionCookie(req), + buildSiteSessionCookie(req), + ].filter(Boolean) + const headers = cookies.length ? { 'set-cookie': cookies } : {} send(res, 200, JSON.stringify({ success: true, ttl: TURNSTILE_SESSION_TTL_SECONDS }), { ...jsonHeaders, ...headers,