updates to api protections
This commit is contained in:
@@ -117,17 +117,18 @@ PUBLIC_DATA_CACHE_STALE_MS=86400000
|
|||||||
PUBLIC_DATA_PREWARM_INTERVAL_MS=300000
|
PUBLIC_DATA_PREWARM_INTERVAL_MS=300000
|
||||||
PUBLIC_DATA_COLD_TIMEOUT_MS=8000
|
PUBLIC_DATA_COLD_TIMEOUT_MS=8000
|
||||||
VITE_STATIC_DATA=false
|
VITE_STATIC_DATA=false
|
||||||
VITE_SITE_GATE=false
|
VITE_SITE_GATE=true
|
||||||
API_RATE_LIMIT_WINDOW_MS=60000
|
API_RATE_LIMIT_WINDOW_MS=60000
|
||||||
API_RATE_LIMIT_MAX=120
|
API_RATE_LIMIT_MAX=120
|
||||||
SITE_SESSION_SECRET=long-random-shared-secret
|
SITE_SESSION_SECRET=long-random-shared-secret
|
||||||
SITE_SESSION_TTL_SECONDS=43200
|
SITE_SESSION_TTL_SECONDS=43200
|
||||||
```
|
```
|
||||||
|
|
||||||
HTML responses set a signed, HttpOnly site-session cookie. `/api/*` and `/data/*`
|
Successful Turnstile verification sets signed, HttpOnly Turnstile and site-session
|
||||||
requests must present that cookie and same-origin browser request metadata, so the
|
cookies. `/api/*` and `/data/*` requests must present those cookies plus
|
||||||
data is served to active site sessions instead of as an open public API. All PM2
|
same-origin browser request metadata, so the data is served to verified active
|
||||||
web instances must share the same `SITE_SESSION_SECRET`.
|
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
|
On startup, the web server preloads the critical public snapshots before
|
||||||
signalling PM2 `ready`: team leaderboard, player leaderboard, home teams, and
|
signalling PM2 `ready`: team leaderboard, player leaderboard, home teams, and
|
||||||
|
|||||||
+1
-1
@@ -64,5 +64,5 @@ DISCORD_INCLUDE_PATCH=true
|
|||||||
# TURNSTILE_SECRET_KEY is the server-only secret used to call the Siteverify endpoint.
|
# TURNSTILE_SECRET_KEY is the server-only secret used to call the Siteverify endpoint.
|
||||||
VITE_TURNSTILE_SITE_KEY=
|
VITE_TURNSTILE_SITE_KEY=
|
||||||
TURNSTILE_SECRET_KEY=
|
TURNSTILE_SECRET_KEY=
|
||||||
VITE_SITE_GATE=false
|
VITE_SITE_GATE=true
|
||||||
VITE_STATIC_DATA=false
|
VITE_STATIC_DATA=false
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ const liveRefreshMs = 15000
|
|||||||
const siteVersion = import.meta.env.VITE_SITE_VERSION || '1.0.0'
|
const siteVersion = import.meta.env.VITE_SITE_VERSION || '1.0.0'
|
||||||
|
|
||||||
const turnstileSiteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY || ''
|
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 staticDataBase = (import.meta.env.VITE_STATIC_DATA_BASE || '/data').replace(/\/+$/, '')
|
||||||
const staticDataEnabled = String(import.meta.env.VITE_STATIC_DATA || 'false').toLowerCase() === 'true'
|
const staticDataEnabled = String(import.meta.env.VITE_STATIC_DATA || 'false').toLowerCase() === 'true'
|
||||||
const missingStaticDataPaths = new Set()
|
const missingStaticDataPaths = new Set()
|
||||||
|
|||||||
+21
-3
@@ -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) {
|
function requireSiteSession(req, res) {
|
||||||
if (!isProtectedDataPath(req)) return true
|
if (!isProtectedDataPath(req)) return true
|
||||||
|
if (isSiteSessionBootstrapPath(req)) return true
|
||||||
|
|
||||||
if (!SITE_SESSION_HMAC_KEY) {
|
if (!SITE_SESSION_HMAC_KEY) {
|
||||||
sendJson(res, 503, { error: 'Site session signing is not configured' })
|
sendJson(res, 503, { error: 'Site session signing is not configured' })
|
||||||
@@ -1299,6 +1310,11 @@ function requireSiteSession(req, res) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isTurnstileSessionVerified(req)) {
|
||||||
|
sendJson(res, 403, { error: 'Turnstile session required' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2469,12 +2485,10 @@ function htmlWithSeo(req, data) {
|
|||||||
|
|
||||||
function sendHtml(req, res, data, status = 200) {
|
function sendHtml(req, res, data, status = 200) {
|
||||||
const html = htmlWithSeo(req, data)
|
const html = htmlWithSeo(req, data)
|
||||||
const siteSessionCookie = buildSiteSessionCookie(req)
|
|
||||||
send(res, status, html, {
|
send(res, status, html, {
|
||||||
...securityHeaders(req, { html: true }),
|
...securityHeaders(req, { html: true }),
|
||||||
'content-type': mimeTypes['.html'],
|
'content-type': mimeTypes['.html'],
|
||||||
'cache-control': 'no-cache',
|
'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 })
|
sendJson(res, 403, { error: 'Turnstile verification failed', detail: verification.error })
|
||||||
return
|
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 }), {
|
send(res, 200, JSON.stringify({ success: true, ttl: TURNSTILE_SESSION_TTL_SECONDS }), {
|
||||||
...jsonHeaders,
|
...jsonHeaders,
|
||||||
...headers,
|
...headers,
|
||||||
|
|||||||
Reference in New Issue
Block a user