fix(p8-010): move rate-limit store to a shared distributed DB store
Replace the per-Vercel-instance in-memory Map rate-limit cache with an atomic shared store backed by the existing Turso RateLimit table, so limits hold across all instances/redeploys and cannot be bypassed by distributing brute-force attempts across instances (audit finding p8-010, MEDIUM). - checkRateLimit now performs a single atomic round-trip: INSERT ... ON CONFLICT(identifier) DO UPDATE ... RETURNING count, reset_at with window-reset semantics (CASE WHEN reset_at < now THEN 1 ELSE count+1). - The DB is now the primary source of truth (no longer a fire-and-forget fallback). The per-instance Map is reduced to a short-TTL local cache used ONLY to fast-fail already-blocked identifiers (cuts DB load during brute- force storms); it can never let a request bypass the limit. - ensureRateLimitSchema() creates the table + a UNIQUE identifier index so ON CONFLICT upserts are well-defined; added RateLimit to db/create.ts. - resetLoginRateLimits / clearRateLimitStore invalidate the local cache. - getClientIP now trusts proxy headers in non-development environments (production + test); local dev stays strict against header spoofing. - bunfig.toml defines import.meta.env.SSR=true so the server-only env guard loads under 'bun test'. - Tests: await clearRateLimitStore in beforeEach (fixes a race where an un-awaited clear let leftover rows corrupt the next upsert); unique test identifiers; realistic remote-shared-store perf bounds; new p8-010 distributed-store tests (restart-survival, multi-instance aggregation, no bypass by alternating instances).
This commit is contained in:
@@ -140,5 +140,21 @@ export const model: { [key: string]: string } = {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_post_id ON PostHistory (post_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_parent_id ON PostHistory (parent_id);
|
||||
`,
|
||||
RateLimit: `
|
||||
CREATE TABLE RateLimit
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
identifier TEXT NOT NULL,
|
||||
count INTEGER NOT NULL DEFAULT 1,
|
||||
reset_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
-- Unique constraint on identifier so ON CONFLICT(identifier) atomic upserts
|
||||
-- (see src/server/security.ts checkRateLimit) are well-defined. This makes
|
||||
-- the rate-limit state shared across all instances (p8-010).
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_ratelimit_identifier_unique ON RateLimit (identifier);
|
||||
CREATE INDEX IF NOT EXISTS idx_ratelimit_reset_at ON RateLimit (reset_at);
|
||||
`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user