The review passes
Security was reviewed ten times as the project grew, most passes by several reviewers at once. The later ones were run blind: reviewers saw the code and nothing else, because the eyes that had not absorbed the project's reasoning were the ones that found the real bugs.
Before the internet: passes one to three
The first review came before remote access was built. It found that a path is an instruction to a
backend holding an admin credential - /api/hls/jellyfin/%252e%252e/System/Info reached
Jellyfin's admin API through a double-decoded dot segment - and that one Jellyfin account serving
films and TV let an id cross between them. It added the first-account setup code, per-account
guess limits, origin-based CSRF checks, no-overwrite uploads, a disk reserve, hashed session keys and
validated reading positions.
The second, broader pass found little. The third found the largest thing in the whole series and it
was architectural: recover() appeared nowhere, so a panic in any goroutine SoundStorm
started - per-source search, the ebook scanner, provisioning, certificate renewal - took the whole
process down. Every such loop now recovers.
The reader, three times
The same class of bug was found three times through three doors, which is why the reader's defences are layered:
- Fourth pass: a book's chapter pointing an absolute
<script src>at the server's own book-resource endpoint ran underscript-src 'self', as the owner. Fixed by refusing non-fetch destinations and never serving a script type. - Fifth pass: the same through a cover - a declared cover named
x.jswas served as JavaScript. Fixed centrally for every stream path. - Sixth pass: a chapter loading the app's own
app.js, which ran against the book's copy of the page's ids. Fixed by stripping scripts from every chapter before rendering and refusing to run the app in a frame.
After remote access: passes five to seven
- Fifth: anyone could switch off renewals for every install by spending the name service's daily challenge budget; cookie tossing from another install's trusted name; state-file growth by any member; OPDS ids as an authenticated GET anywhere; an EPUB directory that cost four times the file's size in memory; and the discovery that no read deadline behind the logging middleware had ever worked.
- Sixth: links out of a book kept
window.opener; a zip64 gap; the Linux installer writing a router's answer into.envunchecked; any member restarting read-along syncs; Jellyfin's trickplay tiles carrying the admin token. - Seventh: the Plex import could reach the compose network; any app on a phone could repoint the Android app through a leftover launch extra; the published APK was debuggable; covers and song headers that exhausted memory; downloads carried over to the next person on a shared device.
Passes eight to ten
- Eighth (whole project):
/static/served the app without its policy; Go and Alpine versions past support; exported volumes world-readable; no ceiling on book and picture reads in flight; Apple TV crashes from a malicious server. - Ninth (eight reviewers, every file): a 16MB PDF that took 2.6GB to read its title; a playlist import that pinned the CPU on a title of brackets; a JPEG of thousands of empty scans; Jellyfin's subtitle manifest leaking its token by another parameter; a panicking shelf fetch that hung every later request; the setup log carrying the setup code.
- Tenth (nine reviewers, every file): a small photo zip that ran the server out of memory at every start; phone backup carrying on into the next person's folder on a shared phone; backup over plain http when a secure name existed; one member slowing everybody through list writes and unlimited video conversions; quota bypass by an upload without a length.
Then: guessing from many addresses
Asked about afterwards as Tor. The throttle already counts guesses per account whatever the address - about 1,400 a day - so many addresses buy a guesser little, but 1,400 a day is plenty against "password1". Three things followed (see accounts):
- Twelve characters and not a common one, checked when a password is set, from a list built in; nothing is fetched.
- New devices can need approval, an owner switch: the right password on a device the account has never used waits until a device already signed in allows it.
- Asking everybody for a new password, and the same request made of anybody who signs in with a password today's rules would refuse.
What the passes taught
- Blind reviewers find more. Context is what makes a reviewer accept a design's assumptions.
- Denial of service by a signed-in member is the most common remaining class: not one expensive operation, but nothing capping how many run at once.
- The same bug returns through a different door; fix the class, centrally, not the instance.
- The fail-open default deserves a test that walks every route - the access check has one.