Redline has no accounts and no login, so there are no passwords or sessions to protect. What it does handle is untrusted input — a PDF from who-knows-where — and a third-party AI processor. The security work is aimed there.
Input is treated as hostile.
- Uploads must be PDFs (magic-byte check, not just the file extension), capped at 3 MB and 40 pages. The page count looks inside Flate-compressed object streams, so a document that hides its real length can't slip the cap, and decompression is bounded by a 32 MB budget so a compression bomb can't be used to exhaust memory.
- The JSON body limit is 4.5 MB — enough for the largest allowed PDF as base64, no more, and the most Vercel accepts anyway.
- The model answers against a fixed JSON schema, so its output is shape-checked before anything renders.
The browser can't be turned against the user.
- A strict
Content-Security-Policyallows scripts only from this origin plus the one inline theme snippet (pinned by its exact SHA-256 hash). Locally the server computes the hash from the shipped file at startup; on Vercel the page is served with the same headers fromvercel.json, andnpm run checkfails if the two ever disagree. Even if a rendered value carried markup, the browser would refuse to execute it. - React escapes all rendered text; there is no
dangerouslySetInnerHTML, noeval, noinnerHTMLanywhere in the codebase. - Every response also sends
X-Content-Type-Options: nosniff,X-Frame-Options: DENY(plusframe-ancestors 'none'),Referrer-Policy: no-referrer, a locked-downPermissions-Policy,Cross-Origin-Opener-Policyand, over HTTPS,Strict-Transport-Security.
Exports are safe.
- CSV export neutralises formula injection: any cell beginning
=,+,-,@or a control character is prefixed with a quote, so a clause a contract smuggled in can't execute when the file is opened in Excel or Sheets.
The free-tier key is defended.
- The
GEMINI_API_KEYlives only in the server environment and never reaches the browser. It is not committed —.envis git-ignored, and the full commit history has been scanned for key material. - A public endpoint with no brakes would let one visitor burn the daily allowance, so requests are rate-limited per IP in separate buckets (5 document reads / 30 questions per 10 minutes) with a global daily ceiling as the backstop.
Logs stay clean. No request body, PDF, or profile is ever logged. Upstream errors are logged as status + a trimmed message only, never the whole error object, so document contents can't leak into stdout.
No SSRF surface. The only outbound call is to Google's Gemini API; no URL is ever taken from user input.
- Rate-limit state is in-memory. On Vercel each function instance keeps its own counters, and instances come and go with traffic, so the per-IP window and the daily ceiling hold per instance rather than across the whole site. Google's own account-level quota is the ultimate backstop; the in-app caps are best-effort, not a hard guarantee.
- The body is parsed before the rate limiter runs, so a flood of large bodies costs some parsing work before being rejected. Bounded by the 4.5 MB limit; acceptable for a personal deployment, not hardened for a hostile internet at scale.
- Prompt injection via the contract is possible in principle — a PDF could contain text aimed at the model. The JSON-schema output and the "quote, never invent" instruction bound the blast radius to a misleading report, not code execution, but a reader should always check quoted clauses against the document (which the app tells them to do).
This is a personal project. If you find a security issue, please open an issue on the repository describing it, or contact the author directly rather than posting exploit details publicly.