github MBombeck/HealthLog v1.4.24
v1.4.24 — Security + accessibility hardening

4 hours ago

Security + accessibility hardening release. Closes the highest-priority findings from a focused security audit of the v1.4.23 release.

Security

  • Bearer auth is fail-closed when a route declares no scope. requireAuth() previously let any non-revoked, non-expired API token through whenever a handler omitted the requiredPermission argument. A narrowly-scoped token (e.g. ["medication:ingest"]) could therefore reach every handler that called requireAuth() bare — including account-sensitive routes such as /api/auth/profile, /api/auth/password, /api/withings/credentials, /api/settings/data, /api/export/full-backup and /api/tokens. The Bearer path now throws HttpError(403) with audit reason scope_required unless the token carries the ["*"] wildcard (the iOS app login token), preserving the existing session-cookie path and the explicit-scope path (/api/ingest/medications). The admin surface is unaffected — requireAdmin() was already cookie-only.
  • Public Umami analytics proxy now rate-limits by client IP. POST /api/send forwards browser analytics events to the configured Umami origin. The SSRF allow-list and 64 KB body cap were already in place, but the endpoint itself was unbounded. Added a 120 events/minute/IP gate; abuse returns the standard 429 envelope without invoking the upstream fetch.
  • Edge-runtime portable nonce generation in the proxy. The proxy previously assembled the CSP nonce via Buffer.from(...), which is not guaranteed in the Edge runtime. Switched to btoa(String.fromCharCode(...new Uint8Array(16))). Same 128 bits of entropy, Node-and-Edge compatible.
  • Session-expiry delete race no longer 500s a parallel request. getSession() reaped expired session rows with prisma.session.delete(); two requests arriving at the same expired session would race and the loser would surface a 500. Replaced with deleteMany + catch-all guard, matching the discipline already used in destroySession().

Fixed

  • POST /api/import returns the standard { data: null, error } envelope on validation failure. The handler previously returned the validation message inside apiSuccess(...) with status 422, which clients could not distinguish from a successful import.
  • Runtime Docker image pins Prisma 7.8 + worker dependencies. The multi-stage image was installing prisma@7.4.0, @prisma/engines@7.4.0, unpinned pg-boss@12, @prisma/adapter-pg@7 and pg@8 into the worker prefix while the app code shipped Prisma 7.8 from the lockfile. Pinned to prisma@7.8, @prisma/engines@7.8, pg-boss@12.18, @prisma/adapter-pg@7.8 and pg@8.20.
  • Notifications page surfaces a load error instead of a silent "no channels" empty state. useQuery failures now render an alert with role="alert" and notifications.loadError copy (EN/DE).

Accessibility

  • Notification-preference switches carry accessible names. Each switch in the per-event table now exposes an aria-label combining event-type label and channel label; the mobile per-event layout pairs each <Switch> with its <Label> via htmlFor/id so screen readers announce the channel a toggle controls.
  • Mobile admin navigation surfaces an Overview link. The mobile admin section pill-rail prepended an Overview chip pointing at /admin.
  • Notification settings deep-link anchors land on the right card. Onboarding and admin-driven deep links into #telegram, #ntfy and #web-push now resolve to the corresponding cards.
  • Chart-overlay comparison-baseline toggles expose aria-pressed.

Changed

  • Per-chart comparisonBaseline overlay preference. The comparison overlay is now configurable per chart from the existing overlay-controls popover instead of being a single global toggle. ChartOverlayPrefs adds a comparisonBaseline: "none" | "lastMonth" | "lastYear" field (defaults to "none"); a per-chart selection overrides the dashboard-level default. ChartOverlayKey is extended with bmi, bodyFat, sleep and steps.
  • Trend-card layout tolerates long labels and long values. TrendCard switches to min-w-0 + flex-wrap containers and [overflow-wrap:anywhere] so locale labels and large tabular numbers no longer push the tile off-axis on narrow viewports.
  • Lint-clean Coach drawer + message thread. useCallback dependency on the drawer's reset closure now includes the stable setInputValue setter; the message thread memoises the derived messages array.

Tests

  • New unit test in require-auth-bearer.test.ts proves that a Bearer token with permissions ["medication:ingest"] is rejected (403 / scope_required) when the route did not declare a scope.
  • New unit suite under src/lib/auth/__tests__/session.test.ts covers the expired-session delete-race path.
  • New unit suite under src/app/api/send/__tests__/route.test.ts covers the rate-limited Umami proxy.
  • Extended import-route test asserts the new apiError envelope.

Verification: pnpm typecheck ✓ — pnpm lint ✓ (0 errors, 0 warnings) — pnpm test ✓ (2244 / 2244) — pnpm format:check ✓.

Known follow-ups (deferred to v1.4.25)

  • Admin data wipe (DELETE /api/admin/data) does not yet cover MoodEntry, UserAchievement, RecommendationFeedback, CoachConversation/Message/Usage, IntegrationStatus, Device.
  • Backup payload / restore handler are asymmetric: restore clears notification channels and push subscriptions that the backup never carried.
  • First-user bootstrap (POST /api/auth/register) decides ADMIN vs USER between two separate Prisma operations without a transactional lock.

Don't miss a new HealthLog release

NewReleases is sending notifications on new releases.