feat(security): add submission validation hardening (cooldown, FPS bounds, settings limits, notes cap)

This commit is contained in:
2026-05-15 00:50:23 +08:00
parent d0b69892ea
commit 75c38341c4
+69
View File
@@ -145,6 +145,75 @@ export const performanceSubmitRoutes = new Elysia({ prefix: "/performance", deta
return { error: "Missing required fields: versionId, hardwareSlug, fpsAvg" } return { error: "Missing required fields: versionId, hardwareSlug, fpsAvg" }
} }
// ── Submission cooldown: 60 seconds between entries per user ──
const sixtySecondsAgo = new Date(Date.now() - 60 * 1000)
const [lastEntry] = await db
.select({ createdAt: performanceEntries.createdAt })
.from(performanceEntries)
.where(
and(
eq(performanceEntries.userId, guard.user.id),
sql`${performanceEntries.createdAt} >= ${sixtySecondsAgo}`,
),
)
.orderBy(sql`${performanceEntries.createdAt} DESC`)
.limit(1)
if (lastEntry) {
const retryAfter = Math.ceil(
(lastEntry.createdAt.getTime() + 60_000 - Date.now()) / 1000
)
set.status = 429
return {
error: "Please wait before submitting another benchmark",
retryAfter: Math.max(1, retryAfter),
}
}
// ── Validation: fpsAvg bounds ───────────────────────────────
if (typeof fpsAvg !== "number" || fpsAvg < 1 || fpsAvg > 500) {
set.status = 400
return { error: "fpsAvg must be between 1 and 500" }
}
// ── Validation: optional FPS bounds ─────────────────────────
if (fpsLow !== null && (fpsLow < 0 || fpsLow > 500)) {
set.status = 400
return { error: "fpsLow must be between 0 and 500" }
}
if (fpsHigh !== null && (fpsHigh < 0 || fpsHigh > 500)) {
set.status = 400
return { error: "fpsHigh must be between 0 and 500" }
}
if (fpsOnePercentLow !== null && (fpsOnePercentLow < 0 || fpsOnePercentLow > 500)) {
set.status = 400
return { error: "fpsOnePercentLow must be between 0 and 500" }
}
// ── Validation: settingsJson size limits ────────────────────
if (settingsJson) {
if (!Array.isArray(settingsJson)) {
set.status = 400
return { error: "settingsJson must be an array" }
}
if (settingsJson.length > 20) {
set.status = 400
return { error: "Maximum 20 settings categories allowed" }
}
for (const cat of settingsJson) {
if (cat.settings && Array.isArray(cat.settings) && cat.settings.length > 50) {
set.status = 400
return { error: `Maximum 50 settings per category (exceeded in "${cat.category}")` }
}
}
}
// ── Validation: userNotes length ────────────────────────────
if (userNotes && typeof userNotes === "string" && userNotes.length > 5000) {
set.status = 400
return { error: "userNotes must be 5000 characters or less" }
}
// Verify the game version exists // Verify the game version exists
const [version] = await db const [version] = await db
.select({ id: gameVersions.id, gameId: gameVersions.gameId }) .select({ id: gameVersions.id, gameId: gameVersions.gameId })