-
v7.1.0
Stablereleased this
2026-07-17 01:00:38 +00:00 | 109 commits to main since this releaseTea Leaves v7.1.0 — Core Hardening & Security
Released 2026-07-16
A pre-v8 hardening pass: fix the daily-use and recovery paths that were quietly broken, and lock down + test the v7 federation trust boundary before stacking v8's moderation layer on top of it. All 13 issues of the milestone, from the 2026-07-15 codebase audit. No new migrations; no breaking changes.
🔒 Security
AP inbox: signer bound to the claimed actor (#82)
verifyInboxSignatureonly proved the request was signed by thekeyIdowner — for every activity type except the entry-write path it never asserted that owner ==activity.actor. Anyone controlling a domain could sign a request while claiming to be an arbitrary third party: spoofed fediverse identities in public motif comments, forced Follow rows, a forged Delete/Undo. The binding is now hoisted to run for all activity types (Follow / Invite / Undo / Like / Announce / Create / Delete) immediately after signature verification →401on mismatch.AP inbox: SSRF guard on remote-controlled fetches (#81)
The unauthenticated inbox fetched attacker-supplied actor URLs and delivered to attacker-supplied inbox URLs with no host filtering, following redirects — blind SSRF into the homelab. New
lib/ssrfGuard.tsresolves the target and rejects loopback / RFC1918 / link-local / ULA / CGNAT, re-validating every redirect hop. Origins an admin has explicitly federated with bypass the denylist, so two homelab instances still federate over a private network (mirrors the #62 outbound allowlist).Backup routes are admin-only (#80)
/api/backup/*sat behind onlyrequireAuth, so any authenticated non-admin (e.g. a project collaborator) could read cloud-storage credentials, manage destinations and stage restores. NowrequireAdmin, andGET /destinationsreturns only the names ofextra_envkeys — never the values (editing is write-only).🛟 Recovery & reliability
Backup restore actually works (#84)
applyRestoreEntryomittedproject_id(NOT NULL →23502) and defaultedcertaintyto'possible', which isn't in the CHECK set (→23514). Every staged entry apply threw — at precisely the moment recovery is needed. Now sourcesproject_idfrom the staged data (falling back to the active project) and validatescertainty, covered end-to-end by an integration test.Graceful shutdown (#85)
Nothing handled SIGTERM, so every deploy hard-killed in-flight import/media/backup jobs, left BullMQ locks lingering, and could strand a backup at
status='running'forever. The API now drains workers, then closes the server, Redis and the pg pool, with a 30 s cap. The handler alone was inert:CMD ["npm", "start"]meant npm — not node — received the signal, so it never ran (exit 1,npm error signal SIGTERM). The image now execs node directly and compose runs tini as PID 1, which also reaps orphaned restic/pg_dump/ffmpeg children.Backups can't hang forever (#86)
restic/pg_dumpspawns had no timeout and noerrorhandler — an unreachable repo or a missing binary left the promise pending forever, silently blocking every future scheduled backup while the dashboard cheerfully showed "running" (so no alert fired). Spawns now have a 2 h kill-timer and surface spawn errors; a reconciliation pass fails any job stillrunningafter 3 h and fires the configured alert.Migrations run before workers (#87)
Workers and the backup scheduler started before
runMigrations(), and the scheduler's error was swallowed — on a first-ever boot backup scheduling silently never registered. Boot order is now migrate → workers → scheduler → listen.📱 Mobile daily-use
Quick Capture no longer throws away voice notes (#90)
Worse than reported: the recorder's target entry was always
nullwhile the panel was open, so every Quick Capture voice note was silently discarded on Stop — the feature had no working path at all. Tapping Record now creates the entry up-front so audio always has somewhere to land, and Save updates that entry rather than duplicating it.Import, Suggestions & Trash reachable on a phone (#89)
All three lived only in the desktop header — on <768px no on-screen control reached them. The bottom nav now ends in a More overflow sheet (Import, Suggestions, Trash, Settings — plus Admin/Federation for admins).
Deep links survive login (#91)
A logged-out user clicking an
/invites/:tokenlink landed on/entrieswith the token gone and no message. The target path now round-trips through the OIDC flow, validated as a same-origin relative path on the way in and out (absolute,//evil.comand/\evil.comrejected) so it can't become an open redirect.🐛 Ops / UI
- Uploads & imports over 1 MB (#83) — the bundled nginx set no
client_max_body_size, so its 1 MB default 413'd essentially every image, video and archive import before it reached the API. Now2gon the/api/block. - PWA install & favicon (#88) — every icon referenced by
index.html/manifest.json404'd; the install prompt and tab icon were broken. Addedfavicon.svg,apple-touch-icon.pngand the 192/512 maskable PNGs.
🧪 Internal
API suite grew to 238 tests (from 232): direct HTTP-signature verification tests (valid / replayed / tampered-digest / unsigned-digest / wrong-key / wrong-actor binding end-to-end), SSRF IP-classification tests, backup authorization + restore-apply coverage, and the open-redirect guard matrix — closing the audit's "the newest perimeter is the least tested" finding (#92).
Upgrading: no migrations and no config changes required. Deploys pick up
init: true/stop_grace_periodfromdocker-compose.yml, which the graceful shutdown depends on — redeploy with the compose file from this tag rather than an older local copy.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Uploads & imports over 1 MB (#83) — the bundled nginx set no