diff --git a/DEVELOPMENT_LOG.md b/DEVELOPMENT_LOG.md index de3d0e3..2ae6371 100644 --- a/DEVELOPMENT_LOG.md +++ b/DEVELOPMENT_LOG.md @@ -6,6 +6,29 @@ --- +### v0.23.2 — Notification Privacy Leak Fix +**Status:** ✅ COMPLETED +**Date:** 2026-05-10 +**Priority:** CRITICAL (Security) + +| Agent | Status | Time | Notes | +|-------|--------|------|-------| +| Neo | ✅ COMPLETED | — | Fixed notification privacy leak in notificationService.js | +| Bishop | ✅ COMPLETED | — | Verified fix, built, tested, version bumped | + +**Files modified:** `services/notificationService.js`, `package.json`, `client/lib/version.js` + +**Work Completed:** +- [x] `services/notificationService.js`: Added ownership filter (`if (allowUserConfig && bill.user_id !== recipient.id) continue;`) — prevents bills from being sent to non-owning recipients in per-user notification mode +- [x] `services/notificationService.js`: Added defensive check for orphaned bills with no `user_id` — warns and skips instead of broadcasting +- [x] Global notification mode (single recipient, `id: 0`) unaffected — filter only applies when `allowUserConfig` is true +- [x] `routes/notifications.js`: Verified — no cross-user data leakage (all endpoints scoped to `req.user.id` or admin-only) +- [x] `client/api.js`: Verified — no endpoints expose notification internals across users +- [x] Docker build passes, container starts, login works, notification endpoints verified +- [x] Version bumped to 0.23.2 + +--- + ### v0.23.1 — Migration Rollback **Status:** ✅ COMPLETED **Date:** 2026-05-10 diff --git a/client/lib/version.js b/client/lib/version.js index d2c1f86..eaee050 100644 --- a/client/lib/version.js +++ b/client/lib/version.js @@ -1,15 +1,11 @@ -export const APP_VERSION = '0.23.1'; +export const APP_VERSION = '0.23.2'; export const APP_NAME = 'BillTracker'; export const RELEASE_NOTES = { - version: '0.23.1', + version: '0.23.2', date: '2026-05-10', highlights: [ - { icon: '🔄', title: 'Migration Rollback', desc: 'Admin API endpoint to rollback supported migrations with transaction safety and audit logging' }, - { icon: '🚀', title: 'Migration Logging Enhancement', desc: 'Detailed migration logging with timing for each migration step, error logging with timing, and total migration time reporting' }, - { icon: '🔧', title: 'Circular Dependency Fix', desc: 'Lazy import pattern for auditService in database.js prevents circular dependency issues' }, - { icon: '🐛', title: 'Skip First-Login for Seeded Users', desc: 'ENV-seeded users (admin, regular) no longer see the first-login flow on container restarts' }, - { icon: '🔒', title: 'Session Rotation on Password Change', desc: 'All other sessions are invalidated when you change your password. Current session gets a new ID.' }, - { icon: '🚪', title: 'Logout All Devices', desc: 'New /api/auth/logout-all endpoint lets you sign out from every device at once.' }, + { icon: '🔒', title: 'Critical: Notification Privacy Leak Fix', desc: 'In per-user notification mode, bills were sent to all opted-in recipients regardless of ownership. Now each recipient only receives their own bills.' }, + { icon: '🛡️', title: 'Orphaned Bill Guard', desc: 'Defensive check added: bills with no user_id are now skipped with a warning instead of being broadcast to all recipients.' }, ], }; \ No newline at end of file diff --git a/package.json b/package.json index 72bcbb6..314181e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bill-tracker", - "version": "0.23.1", + "version": "0.23.2", "description": "Monthly bill tracking system", "main": "server.js", "scripts": { diff --git a/services/notificationService.js b/services/notificationService.js index 0cee7f4..07a163d 100644 --- a/services/notificationService.js +++ b/services/notificationService.js @@ -177,6 +177,9 @@ async function runNotifications() { const { getCycleRange, resolveDueDate } = require('./statusService'); const { start, end } = getCycleRange(year, month); + // Fetch all active bills. In global-notification mode, the single global recipient + // legitimately receives every bill. In per-user mode, each recipient must only + // see their own bills — the ownership filter is applied in the loop below. const bills = db.prepare('SELECT * FROM bills WHERE active = 1').all(); const allowUserConfig = getSetting('notify_allow_user_config') === 'true'; const globalRecipient = getSetting('notify_global_recipient'); @@ -223,7 +226,16 @@ async function runNotifications() { if (!type) continue; + // Defensive: warn if a bill somehow has no owner + if (!bill.user_id) { + console.warn(`[notifications] Bill id=${bill.id} name="${bill.name}" has no user_id — skipping`); + continue; + } + for (const recipient of recipients) { + // In per-user mode, only send bills belonging to this recipient + if (allowUserConfig && bill.user_id !== recipient.id) continue; + // Check recipient's preferences if (type === 'due_3d' && !recipient.notify_3d) continue; if (type === 'due_1d' && !recipient.notify_1d) continue;