Skip to content

1.3.1 — Full audit hardening pass (security, reader, performance, UX)#6

Draft
SethMorrowSoftware wants to merge 1 commit into
mainfrom
claude/project-audit-review-IW2Bq
Draft

1.3.1 — Full audit hardening pass (security, reader, performance, UX)#6
SethMorrowSoftware wants to merge 1 commit into
mainfrom
claude/project-audit-review-IW2Bq

Conversation

@SethMorrowSoftware
Copy link
Copy Markdown
Owner

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

  • Atomic single-use claim for password-reset tokens. AuthTokenRepository::claimSingleUse does UPDATE … WHERE used_at IS NULL and checks rowCount() === 1; closes the find-then-mark race in reset-password.php.
  • Admin "reset password" no longer leaks the new password. The action is now send_reset_link — issues a one-time password_reset token (same path as public forgot-password) and emails it via the Mailer (works under any driver, including log in dev).
  • CSRF query-string fallback narrowed to a small allow-list (currently just api/sessions.php for the sendBeacon heartbeat). Tokens can no longer leak via Referer from arbitrary POSTs.
  • Input length caps enforced at the controller layer with clear errors:
    • Review title ≤ 200, body ≤ 10 000 (api/reviews.php, book.php)
    • Collection name ≤ 120, description ≤ 500 (collections.php, CollectionRepository)
  • Page/offset DoS hardening: api/reviews.php clamps offset against the row total; BookRepository::paginate already clamped page (verified).
  • .phar blocked in root .htaccess (matches existing .phtml/.phps/.php3..7/.inc denial).

Reader robustness

  • Service worker cache busts on deploy. sw-register.js registers sw.js?v=<APP_VERSION>; sw.js parses the v= param into cache keys. Bumping includes/version.php automatically invalidates stale shells.
  • Highlights panel aria-busy loading state while the initial fetch resolves (was: blank until response).
  • TTS pause/resume label derives from synth.paused — stays in sync after skip / rate-change / visibilitychange.
  • TTS voice select shows "Loading voices…" until voiceschanged fires (Chrome first paint).
  • In-book search times out a stuck chapter after 8 s and continues; one broken item.load() no longer hangs the whole search.
  • Dictionary popover dismisses on Escape, outside touch, and taps inside the reader iframe; adds aria-modal="true".
  • Progress click-to-seek surfaces invalid CFI / load failures via toast (was: silent).
  • Highlights roll back optimistic UI on save/update/delete failure, with a clear "reverted" toast.

Performance & DB

  • New migration 0015_audit_hardening.sql adds four missing indexes on hot paths:
    • book_tags(book_id, tag_id) — book-detail tag fetches
    • reading_progress(percentage) — stats / Continue Reading
    • collection_books(collection_id, added_at) — date-sorted shelves
    • reviews(updated_at) — admin "recently edited" filters
  • N+1 removed on /collections.php — batched CollectionRepository::previewCoversFor.
  • Admin → Maintenance page (/admin/maintenance.php) purges expired auth_tokens and rate_limits. Linked from the admin sidebar.
  • Slug-collision retry in CollectionRepository::create.

Setup, version, 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; SW reads it.
  • setup_render_locked() tells installers to delete setup.php.
  • INSTALL.md documents the maintenance cron.
  • SECURITY.md adds a "Post-install hardening" checklist; drops the obsolete "no email password reset" item.
  • CHANGELOG.md 1.3.1 entry.

UI/UX

  • Reader hotspots gain a visible :focus-visible inset indicator.
  • Auth layout focuses the .form-errors[role="alert"] container on submit-with-errors.
  • Library autocomplete listbox flips above the input near the viewport bottom.

Test plan

  • php -l on every changed .php file — done locally, all green.
  • node --check on every changed .js file — done locally, all green.
  • Apply migration 0015 via /admin/migrate.php; drift = 0.
  • Request password reset, click the link twice rapidly in two tabs — only one succeeds; second shows "no longer valid".
  • /admin/users → "Send reset link" → confirm message says "Reset link sent to …"; verify the link in storage/logs/mail.log (or inbox if mail/smtp driver).
  • Submit /login.php with blank fields → focus lands on the error container; visible focus ring shows.
  • Open a book → highlights panel shows "Loading highlights…" then resolves; verify aria-busy toggles.
  • Toggle TTS pause rapidly → label flips Pause/Resume reliably.
  • Run in-book search with one chapter throttled via DevTools → moves on after ~8 s, shows "(N skipped)".
  • Open dictionary popover → Escape and outside-tap dismiss it on touch + desktop.
  • Drag progress bar to a garbage position → toast appears.
  • Voice select shows "Loading voices…" on first open in Chrome.
  • Save a highlight with the server offline → toast says "Save failed", local store reverts.
  • SW: bump APP_VERSION, reload → DevTools shows a new SW with new cache keys.
  • EXPLAIN queries: book-detail tags, continue-reading, date-sorted shelf, recently edited reviews — all use the new indexes.
  • Admin → Maintenance → "Purge expired auth tokens" / "Purge expired rate limits" → counts drop; audit log entry written.
  • Try ?page=999999999 on /search.php → clamped silently to the last page.
  • POST /api/reviews.php with a 100 KB body → 422 with length-cap message.
  • Revisit /setup.php after install → locked page warns to delete the file.

Generated by Claude Code

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants