1.3.1 — Full audit hardening pass (security, reader, performance, UX)#6
Draft
SethMorrowSoftware wants to merge 1 commit into
Draft
1.3.1 — Full audit hardening pass (security, reader, performance, UX)#6SethMorrowSoftware wants to merge 1 commit into
SethMorrowSoftware wants to merge 1 commit into
Conversation
Security & auth: - Atomic single-use claim for password reset tokens (closes find-then-mark race) - Admin "reset password" now emails a one-time reset link instead of displaying the new password in a flash message - CSRF query-string fallback restricted to beacon endpoints only - Review/collection length caps enforced at controller + repo - Paginated review offset clamped against row total - .phar blocked in root .htaccess Reader robustness: - Service worker cache busts via sw.js?v=APP_VERSION - Highlights panel shows a loading state until the initial fetch resolves - TTS pause/resume label derives from synth.paused (was: out of sync) - TTS voice select shows "Loading voices..." until voiceschanged fires - In-book search times out a stuck chapter after 8s and continues - Dictionary popover dismisses on Escape / outside touch / iframe taps - Progress bar click-to-seek surfaces failures via toast - Highlights roll back optimistic UI on save/update/delete failure Performance & DB: - New migration 0015 adds four missing indexes (book_tags/book, progress percentage, collection_books/added_at, reviews/updated_at) - Shelves index N+1 removed via CollectionRepository::previewCoversFor - New Admin -> Maintenance page purges expired auth_tokens / rate_limits - CollectionRepository::create retries on slug uniqueness collision Setup, docs: - includes/version.php is the single source of APP_VERSION - setup.php writes the running version (was hardcoded 1.0.0) - <meta name="app-version"> on every layout - SECURITY.md gains a "Post-install hardening" checklist - INSTALL.md documents the periodic maintenance cron - CHANGELOG.md 1.3.1 entry UI/UX: - Reader hotspots get a visible :focus-visible indicator - Auth pages auto-focus the error container on validation failure - Library search autocomplete listbox flips above the input near viewport bottom
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A full security, reliability, accessibility, and performance pass on top of 1.3.0. No new features — every change tightens, clarifies, or speeds up something that already shipped. Three parallel Explore audits surfaced the findings; each was personally verified before fixing.
The codebase has a strong foundation (prepared statements everywhere, Argon2id, split-token remember-me, CSRF, security headers with CSP nonce, audit logging, rate limiting). This branch is hardening + polish, not a rewrite. 35 files changed, +683 / −101.
Security & auth
AuthTokenRepository::claimSingleUsedoesUPDATE … WHERE used_at IS NULLand checksrowCount() === 1; closes the find-then-mark race inreset-password.php.send_reset_link— issues a one-timepassword_resettoken (same path as public forgot-password) and emails it via theMailer(works under any driver, includinglogin dev).api/sessions.phpfor the sendBeacon heartbeat). Tokens can no longer leak via Referer from arbitrary POSTs.api/reviews.php,book.php)collections.php,CollectionRepository)api/reviews.phpclampsoffsetagainst the row total;BookRepository::paginatealready clamped page (verified)..pharblocked in root.htaccess(matches existing.phtml/.phps/.php3..7/.incdenial).Reader robustness
sw-register.jsregisterssw.js?v=<APP_VERSION>;sw.jsparses thev=param into cache keys. Bumpingincludes/version.phpautomatically invalidates stale shells.aria-busyloading state while the initial fetch resolves (was: blank until response).synth.paused— stays in sync after skip / rate-change / visibilitychange.voiceschangedfires (Chrome first paint).item.load()no longer hangs the whole search.aria-modal="true".Performance & DB
0015_audit_hardening.sqladds four missing indexes on hot paths:book_tags(book_id, tag_id)— book-detail tag fetchesreading_progress(percentage)— stats / Continue Readingcollection_books(collection_id, added_at)— date-sorted shelvesreviews(updated_at)— admin "recently edited" filters/collections.php— batchedCollectionRepository::previewCoversFor./admin/maintenance.php) purges expiredauth_tokensandrate_limits. Linked from the admin sidebar.CollectionRepository::create.Setup, version, docs
includes/version.phpis the single source ofAPP_VERSION.setup.phpwrites the running version (was hardcoded1.0.0).<meta name="app-version">on every layout; SW reads it.setup_render_locked()tells installers to deletesetup.php.INSTALL.mddocuments the maintenance cron.SECURITY.mdadds a "Post-install hardening" checklist; drops the obsolete "no email password reset" item.CHANGELOG.md1.3.1 entry.UI/UX
:focus-visibleinset indicator..form-errors[role="alert"]container on submit-with-errors.Test plan
php -lon every changed.phpfile — done locally, all green.node --checkon every changed.jsfile — done locally, all green.0015via/admin/migrate.php; drift = 0./admin/users→ "Send reset link" → confirm message says "Reset link sent to …"; verify the link instorage/logs/mail.log(or inbox ifmail/smtpdriver)./login.phpwith blank fields → focus lands on the error container; visible focus ring shows.aria-busytoggles.APP_VERSION, reload → DevTools shows a new SW with new cache keys.EXPLAINqueries: book-detail tags, continue-reading, date-sorted shelf, recently edited reviews — all use the new indexes.?page=999999999on/search.php→ clamped silently to the last page.POST /api/reviews.phpwith a 100 KB body → 422 with length-cap message./setup.phpafter install → locked page warns to delete the file.Generated by Claude Code