* fix(library): count only uploaded, non-deleted books in synced toast The "N book(s) synced" toast shown on pull-to-refresh and the last-synced menu counted every non-deleted book record a pull returned, including books indexed in the cloud as metadata only whose file blob was never uploaded. Those books are never added to the library (updateLibrary requires uploadedAt && !deletedAt), so the toast over-reported. Extract the count into a pure countSyncedRecords(type, records) helper that excludes deleted records and, for books only, requires uploaded_at — matching what actually lands in the library. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(memory): record agent notes for recent fixes Persisted project-memory docs accumulated alongside recently merged work (biometric app-lock, download-file scope regression, inline-block column overflow, iOS instant-dict double popup, RSVP RTL words, web security advisories) plus MEMORY.md/share-feature index updates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.9 KiB
name, description, metadata
| name | description | metadata | ||||||
|---|---|---|---|---|---|---|---|---|
| security-advisories-web-2026-06 | GHSA advisory fixes (OPDS SSRF, storage traversal, Stripe hijack, Tauri IPC scoping) — PRs #4638 (web) + #4639 (native); canonical isBlockedHost location |
|
Five GitHub security advisories filed against readest (2026-06-06/09), all triage when worked on 2026-06-18. Four distinct issues (GHSA-c7mm & GHSA-5g3f are the same OPDS-proxy SSRF). ALL FOUR FIXED across two PRs (base main):
- PR #4638
fix/security-advisories-web— web/server: A (OPDS SSRF + kosync isLanAddress), B (storage), D (Stripe). - PR #4639
fix/security-tauri-transfer-scope— native: C (Tauri transfer_file scoping).
After both merge: comment on each GHSA noting the fixing PR (pending merge). Note: a stray git checkout dev happened during the first lint-staged commit (commit landed on dev, moved back to feature branch via git branch -f) — double-check git branch --show-current before committing here.
Fixed (A, B, D):
- A — OPDS proxy SSRF (
src/app/api/opds/proxy/route.ts): added scheme allowlist +isBlockedHost+ manual per-hop redirect (fetchFollowingRedirects/SsrfBlockedError). Closes the self-hosted Critical (IMDS/internal/redirect-SSRF). Reference safe pattern wassrc/pages/api/send/fetch-url.ts. - A2 — canonical SSRF blocklist: moved
isBlockedHost/isBlockedV4intosrc/utils/network.ts(canonical home).fetch-url.tsnow imports + re-exports it (keepssend-fetch-url-guard.test.tsimport valid). ReimplementedisLanAddress(url)to delegate toisBlockedHost— fixes the kosync secondary finding (old one only blocked literal127.0.0.1/0.0.0.0, missed127.0.0.0/8,198.18/15, multicast,.internal/.lan/single-label, mapped IPv6). Safe:new URL()throws on invalid-octet IPs so the catch keeps10.256.0.1→false (existingnetwork.test.tsstays green).KOSyncClienttreats LAN as fetch-direct;CatalogManageronly warns → stricter is safe. - B — storage upload key traversal (
src/pages/api/storage/upload.ts): addedisSafeObjectKeyNameinsrc/utils/object.ts, guard after auth (covers temp + main). KEY CONSTRAINT: legitfileNameDOES contain/(Readest/Books/...,Readest/Replicas/<kind>/<id>/<file>fromuploadReplicaFile), so reject.././empty segments + leading-slash + backslash + NUL (raw & percent-decoded), NOT all slashes. - D — Stripe
/checkhijack (src/app/api/stripe/check/route.ts): addedif (session.metadata?.userId !== user.id) return 403before createOrUpdate*. checkout route stampsmetadata.userId; webhook already binds on it.
C — Tauri native (PR #4639) GHSA-55vr-pvq5-6fmg: unscoped download_file/upload_file in src-tauri/src/transfer_file.rs → arbitrary local read/write. FIXED: added app: AppHandle param + ensure_path_allowed (rejects relative + .. via has_disallowed_components, then fs_scope().is_allowed()). Chose STRICT is_allowed (NOT read_dir's || contains("Readest") substring hatch) because all legit callers (cloud sync, WebDAV, self-updater APK→'Cache', OPDS→'Cache') resolve under static scope ($APPDATA/Readest, $APPCACHE, $TEMP) OR persisted dialog grants (custom root via setCustomRootDir→picker→allow_paths_in_scopes; external folders re-granted at startup; tauri_plugin_persisted_scope makes sticky). Clippy needed #[allow(clippy::too_many_arguments)] on download_file (8 args). AppHandle auto-injected → JS invoke unchanged. NOTE: shared target/ (worktree) was polluted with a deleted sibling worktree's abs plugin-permission paths → build failed failed to read .../readest-feat-nightly-update-channel/.../fs/permissions/app.toml; fix = rm -rf the target/debug/build/<pkg>-<hash> dirs grepping for the stale path, then rebuild. skip_ssl_verification left as-is (OPDS needs it). read_dir's own contains("Readest") hatch left untouched (out of scope).
Non-obvious decision: OPDS proxy can't require Readest auth — it's consumed from the browser via <img src={getProxiedURL(...)}> (covers) and window.fetch WITHOUT a Readest token; the auth query param is the upstream OPDS server cred, not the user token. So auth would break OPDS browsing/images. SSRF host-filter is the non-breaking high-value fix; residual relay/CORS-bypass on hosted CF (Medium) left for maintainer. On web the proxy is a CF Worker that can't reach a user LAN anyway (desktop bypasses via needsProxy), so blocking private hosts removes no functionality.
Test invocation gotcha: npx vitest run <file> skips dotenv → src/utils/supabase.ts:8 atob(...) throws at import for tests that load the REAL @/utils/access (e.g. send-fetch-url-guard.test.ts). Use pnpm test (wraps dotenv -e .env -e .env.test.local) or npx dotenv -e .env -e .env.test.local -- vitest run. Tests that mock supabase/access are unaffected.