What began as a second same-day patch grew into a fuller batch: a
security-hardening sweep of the API surface, two author-metadata cleanups from
a first-time contributor, the naming-template fix behind several import
complaints, and the original cut's fixes (a concurrent-import crash, bulk
folder import rejecting its obvious targets, and a defence-in-depth guard
behind the v1.23.2 data-loss fix).
Security
- Recommendations are scoped to the signed-in user (#1384) — every
recommendation handler (list, dismiss, refresh, clear dismissals, author
exclusions) hardcoded user id1, so on a multi-user install all users
shared the admin's Discover feed and could read or mutate each other's
dismissals and exclusions. It was the only handler family that ignored the
session identity; all eight call sites now thread
auth.UserIDFromContext, matching how authors and history scope their data. - Root-folder, Grimmory, and Calibre integration routes are admin-only
(#1387) —POST/DELETE /rootfolder, the Grimmory config/test endpoints, and
the Calibre test/import/sync endpoints sat outsideRequireAdmin, letting
any authenticated non-admin register or delete server storage roots, rewrite
an integration URL + credential, or trigger credentialed probes and bulk
pushes. They now sit behind the same admin boundary the rest of the
infrastructure routes (indexers, download clients, notifications, backups)
already enforce. - Login timing no longer reveals which usernames exist (#1386) — a login
attempt against a non-existent username skipped the argon2id hash entirely
and returned in microseconds, while a real username paid the full hash cost —
a timing side-channel measurable over the network, on both the main login
and the OPDS basic-auth path. A missing user now verifies against a dummy
hash so every attempt costs the same. - 500 responses no longer echo internal error detail (#1385) — roughly 230
handlers returnederr.Error()straight to the client on internal failures,
leaking SQLite table/column/constraint text and absolute filesystem paths.
Server errors now return a genericinternal server errorbody and log the
full detail server-side (with the request method and path) instead.
CI
- Docs-only PRs can merge again (#1380) —
Security Summaryis a required
status check, but the security workflow path-ignores docs-only diffs, so a
PR touching only Markdown/docs never received the check and sat permanently
blocked. A companion workflow now runs on exactly the inverted path set and
reports the check green — there is nothing to scan in a docs-only diff.
Fixed
-
Concurrent imports no longer crash on author writes (#1374) — the
accent-folding sort key added in v1.23.1 shared a single
x/text/transform.Chainacross the process, but that transformer mutates
internal buffers and is not safe for concurrent use. Two goroutines writing
authors at once (an ABS import, parallel author work discovery, simultaneous
API calls) could corrupt its state and panic with slice-bounds errors inside
x/text/transform, killing the importing goroutine. The transformer is now
built per call; a 16-goroutine regression test pins it under-race. -
Bulk folder import accepts the library root and the download folder
(#1373) — the bulk import shipped in v1.23.0 rejected both of its obvious
targets withpath is outside the configured library roots: the containment
check reused the delete path's rule that a root itself is never a valid
target, so pasting/booksfailed, and the download dirs
(BINDERY_DOWNLOAD_DIR/BINDERY_AUDIOBOOK_DOWNLOAD_DIR) were never in the
allow-list at all — even though a migration backlog in/downloadsis the
exact use case the feature was built for. Scanning a configured root or a
download dir now works; the delete handlers keep the stricter library-only
containment. -
Every book-file delete path now refuses to remove a file another book
owns (#1368 hardening) — v1.23.2 added the ownership check only in the
book-delete legacy-column fallback. The invariant now lives in the shared
delete chokepoint, so the per-file delete, author delete, and Fix Match
cleanup are covered too: if a path is still registered to a different book in
book_files, the on-disk delete is skipped (and it fails safe when ownership
can't be determined). -
Wanted and Queue pages no longer race their own polling (#1382) — both
pages poll every 5 seconds and wrote fetch results straight into state, so a
slow response landing after unmount (or after a newer poll already resolved)
could clobber fresher rows or set state on a dead component. Both effects now
carry the samecancelledcleanup flag AuthorsPage already uses. -
Author search collapses duplicated provider-noise names (#1359, thanks
@pcamp96) — OpenLibrary occasionally carries junk author records whose name
repeats the whole token sequence (Black, Chuck, Black, Chuck). These now
dedup into the clean author record instead of appearing as a second result,
and the clean label wins when the two records merge. -
Author refresh prunes same-name technical collisions (#1360, thanks
@pcamp96) — when OpenLibrary groups an unrelated same-name person's works
under an author (a fiction catalogue suddenly containing a computer-networking
textbook), the refresh now drops the obvious subject outlier instead of
auto-creating it as a wanted book. Conservatively gated: only fires on
fiction-heavy catalogues, and any fiction signal on the work exempts it. -
The ebook naming template configured in Settings is honoured (#1391,
closes #1356) — the Settings UI has saved the ebook naming template under
naming.bookTemplatesince v0.2.0, but the importer readnaming_template—
a key nothing writes. Every ebook import (including Fix Match, where #1356
caught it moving a correctly-placed file out of the configured series
layout) silently used the built-in default layout, while the client-side
preview showed the configured template applying. The importer now reads the
key the UI writes (keeping the old key as a legacy fallback); as before, a
template change applies from the next restart.