Hardening j7: an efficiency + security audit log
- #engineering
- #security
- #performance
We ran a three-pass audit of the j7 codebase — one sweep each for the backend, the web/SEO layer, and data integrity — then shipped every fix the same day. This is the engineering log.
The one that mattered: a stored XSS
Our inline-markdown renderer escaped &, <, and > before turning [text](url) into a link — but not the double-quote. A crafted URL could close the href attribute and inject an event handler that runs in a reader's session. Because posts have a collaborator invite flow, this was a genuine stored XSS, not merely self-XSS. The fix: escape quotes before applying markup and exclude quotes from the URL match — in both the Astro and React renderers, which stay in lockstep.
Highs — one request should never be able to hurt the box
j7's backend runs on a deliberately tiny box, so the bar is simple: no single request may pin the CPU or exhaust memory.
- Image uploads reject over-40MP inputs before decode (a small-on-disk decompression bomb used to expand to over a gigabyte of raw pixels) and optimize one at a time.
- A hard 16MB request-body ceiling, an error-net so a stray throw can't crash a path, and a SQLite busy-timeout instead of instant lock errors.
- The public compute-heavy endpoints (leaderboard, stats) are cached; the on-demand social-card renderer sheds load past a concurrency cap.
- Owner lookups moved from scanning every space to an indexed column, and 'where is this image used' is now owner-scoped and auth-gated.
Before / after
| Area | Before | After |
|---|---|---|
| Image upload | decode bombs could OOM | 40MP cap + serialized |
| Leaderboard / stats | full scan every hit | cached (5m / 2m) |
| Owner lookup | scan all spaces in JS | indexed column search |
| File serving | sync full-file buffer | streamed byte-range |
| Table / list size | unbounded | capped at save time |
Content is now bounded
Every block that holds a collection — table rows, list items, schedule events — now has a size cap enforced at save time, and event colors are validated to a hex value so they can't be used as a CSS-injection vector. A malformed or oversized draft is rejected before it can bloat storage or every future page render.
The quiet efficiency wins
- Space resolution is cached, so a cold-cache render stops re-asking the backend who owns the host.
- A/B-experiment lookups are negative-cached for 60s — most posts have no experiment, so that per-view round-trip is gone.
- Every cross-service fetch now has a timeout, so a hung backend can't stall a page render forever.
- The editor's crash-recovery backstop no longer serializes the whole document to disk on every keystroke.
None of this changes what j7 does. It changes how much it can take before it flinches — which is the whole point of a hardening pass. Onward.