Phase 1 security hardening: dead secrets files deleted, body limits, CSP, error redaction

Three commits closing the bulk of an upstream security audit. The most urgent finding - two dead frontend files that referenced god-mode credentials - is fixed first. The rest cover body size limits, zip/XML bomb guards, JWT email validation, a CSP policy, multer upgrade, and scrubbing raw Postgres errors from API responses.

securityinfrastructure

C3 (critical) - dead secrets surface (204351ca, +9/-173 across 4 files). frontend/src/lib/supabase-server.ts and frontend/src/lib/storage.ts had zero imports but referenced SUPABASE_SECRET_KEY and R2_SECRET_ACCESS_KEY. Any future client-side import would have leaked both to every browser session. Both files are deleted. .env.local.example gains a header explicitly stating that service-role keys, database passwords, storage credentials, and AI provider keys belong in backend/.env only.

H2/H3/H4/H5/H7/M5/M6 (a46810a5, +365/-79 across 11 files). Several fixes in one:

  • CORS now refuses to start in production without FRONTEND_URL set.
  • Global JSON body limit drops from 50 MB to 1 MB; chat gets a 10 MB per-route override.
  • LibreOffice DOCX conversion races against a 60-second timeout.
  • New backend/src/lib/safeZip.ts provides loadZipSafely (rejects archives with declared uncompressed size over 200 MB) and assertSafeXml (rejects payloads over 50 MB or containing a DOCTYPE or ENTITY declaration). All four JSZip.loadAsync call sites in docxTrackedChanges.ts migrate to it. Standard XML entities in legitimate DOCX content continue to work.
  • JWT email is validated for shape before being interpolated into the PostgREST cs filter on shared_with.
  • multer upgraded from 1.x to 2.x (1.x has documented DoS CVEs).
  • Helmet's CSP goes from contentSecurityPolicy: false to default-src: 'none'; frame-ancestors: 'none' - correct for a JSON API surface.

M3/M4 (35712cbe, +87/-3571 across 10 files). Twenty-four routes across chat, projects, documents, tabular, workflows, user, and review-creation paths previously returned raw Postgres error messages. All 24 migrate to sendServerError, which logs the full error server-side and returns a generic message to the client. devLog and devWarn helpers in backend/src/lib/logger.ts no-op when NODE_ENV === "production" - all console.log calls in routes/documents.ts and lib/chatTools.ts are migrated. console.error is kept where appropriate so operational errors stay visible. backend/bun.lock and frontend/bun.lock are also deleted in this commit (PIP standardises on npm).

So what The dead-secrets file deletion and the body-limit + zip/XML guards are zero-risk ports that don't depend on any PIP-specific scaffolding. If your fork still has the upstream `supabase-server.ts` or `storage.ts` in the frontend tree, check now. The `sendServerError` / `devLog` pattern is a slightly bigger touch (24 routes) but straightforward to apply. The CSP change from disabled to `default-src: 'none'` is correct for a pure API backend; verify it doesn't break any HTML responses your fork serves through the same Express app before applying.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

3 commits from cpatpa/PIP, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
204351ca Remove dead frontend secrets surface (C3, critical) Claude 2026-05-14 ↗ GitHub
commit body
Two frontend modules referenced privileged secrets but had zero
importers anywhere in frontend/src/:

- frontend/src/lib/supabase-server.ts read SUPABASE_SECRET_KEY (the
  Supabase service-role key, full god mode on the database).
- frontend/src/lib/storage.ts read R2_SECRET_ACCESS_KEY and instantiated
  an S3 client at module-evaluation time.

Their continued presence meant any future import from a client
component would silently leak these secrets to every browser session.
Both files deleted. SUPABASE_SECRET_KEY removed from
frontend/.env.local.example. README updated to make explicit that the
service-role key belongs in backend/.env only.

Closes security finding C3.
a46810a5 Harden backend against audit findings H2, H3, H4, H5, H7, M5, M6 Claude 2026-05-14 ↗ GitHub
commit body
CORS (H2): the backend now refuses to boot in production without
FRONTEND_URL set, and accepts a comma-separated allowlist of origins.
No localhost fallback in production.

JSON body limit (H3): global limit reduced from 50 MB to 1 MB. Chat
endpoints get a 10 MB override via a per-route parser. Both limits are
env-configurable.

LibreOffice conversion timeout (H4): docxToPdf now races the soffice
call against a hard timeout (default 60 s) so a malformed DOCX cannot
hang the request indefinitely. Throws DocxConversionTimeoutError on
expiry; existing call sites already log-and-continue on conversion
failure.

DOCX zip and XML guards (H5): new safeZip.ts wraps JSZip.loadAsync
behind loadZipSafely (rejects archives whose declared uncompressed
footprint exceeds DOCX_MAX_UNCOMPRESSED_BYTES, default 200 MB) and
adds assertSafeXml (rejects payloads above DOCX_MAX_XML_BYTES, default
50 MB, or containing DOCTYPE or ENTITY declarations). All four
JSZip.loadAsync sites in docxTrackedChanges.ts migrated. Predefined
XML entities continue to be processed correctly.

Email shape validation (H7): the JWT-supplied email is now validated
against a conservative shape before being interpolated into the
PostgREST `cs` filter on shared_with.

Multer upgrade (M5): bumped from 1.4.5-lts to 2.x. The 1.x line has
known DoS CVEs. API surface unchanged for our usage.

Helmet CSP (M6): replaced contentSecurityPolicy:false with a strict
default-src:'none'; frame-ancestors:'none' CSP on all API responses.

Supporting infrastructure: new logger.ts exposes devLog/devWarn that
no-op in production, and httpErrors.ts provides sendServerError and
friends so routes can be migrated off raw error message exposure.

Updated docs/security/01-threat-model.md and CHANGELOG.md with
remediation details.
35712cbe Redact route error responses and production logs (M3, M4) Claude 2026-05-14 ↗ GitHub
commit body
M3: routes no longer return raw Postgres error messages to clients.
The 24 sites of the
  if (error) return void res.status(500).json({ detail: error.message });
pattern across chat, projects, documents, tabular, workflows, user, and
tabular review-creation now use the sendServerError helper. The full
error is logged server-side with context; the client receives a
generic message.

M4: console.log calls in routes/documents.ts and lib/chatTools.ts
migrated to devLog. devLog is a no-op when NODE_ENV=production, so
document content excerpts, storage paths, and edit-resolution payloads
no longer leak to stdout in production. console.error retained where
appropriate.

Also removed backend/bun.lock and frontend/bun.lock. PIP standardises
on npm.

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-359.md from inside the repo you want the changes in.

⬇ Download capture-thread-359.md