Security fixes: filename sanitization, timing-safe HMAC, HKDF salts, RLS deny-all
Dshamir integrates four upstream PRs addressing concrete vulnerabilities: a prompt-injection vector via crafted filenames, a length-oracle side channel in download token verification, weak API key encryption, and an open PostgREST data plane with no row-level access control.
The filename sanitization (af4ed2d, PR #158) adds an 8-line sanitize.ts that strips control characters, collapses whitespace, NFC-normalizes, and truncates strings before they reach LLM system prompts. The target is uploaded document filenames - a PDF named with embedded newlines or instruction text could manipulate the system prompt. The diff shows ~5400 changed lines in chatTools.ts, but that is almost entirely CRLF normalization; the functional change is the sanitizeLlmInput() call at the injection points.
The crypto fixes (52f47ba) address three separate issues. Download token verification was calling crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)) after an early-exit length check - a timing oracle that leaks signature length. The fix pads both buffers to Math.max(len_a, len_b) before the comparison, then checks length equality separately. The API key encryption moved from SHA-256 key derivation to HKDF with a random 16-byte per-row salt stored in a new salt column on UserApiKey: crypto.hkdfSync("sha256", secret, salt, "ailegal-api-key-v1", 32). Legacy rows without a salt fall back to the old SHA-256 path for backward compatibility. Case-insensitive email comparison in shared_with project access (PR #79) prevents access bypasses via email casing.
The data-plane hardening (76fedfc) adds an RLS deny-all SQL script that runs ALTER TABLE ... ENABLE ROW LEVEL SECURITY and FORCE ROW LEVEL SECURITY on every non-Prisma table in the public schema, plus an event trigger that auto-applies RLS to any future table. This specifically blocks the PostgREST anon role from reading anything directly. Prisma's service-role connection bypasses RLS and is unaffected. The same commit adds withStreamTimeout() - a Promise.race wrapper with a 180-second default - applied to runLLMStream() to prevent hung SSE connections, and caps document_ids in the download-zip endpoint at 50 to prevent memory exhaustion.
The keyset pagination (eb70e06, PR #110) adds a before cursor parameter (ISO timestamp) to GET /chat for scrolling through chat history without offset drift. Zod validation on POST /projects/:id/chat (PR #155) also lands here, using the zodProjectChatBody schema.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?