fix: notification privacy leak — per-user bills no longer sent to all recipients (v0.23.2)
CRITICAL security fix: In per-user notification mode, the notification runner was fetching ALL active bills globally and sending each bill's details to every opted-in recipient regardless of ownership. This meant User A's bill names, amounts, and due dates could be emailed to User B. Fix: Added ownership filter in the recipient loop: if (allowUserConfig && bill.user_id !== recipient.id) continue; Also added a defensive guard for bills with no user_id (orphaned bills), which are now skipped with a console.warn instead of being broadcast. Global notification mode (single admin recipient) is unaffected. Security audit: Private_Hudson confirmed the fix is airtight. All other routes (bills, payments, tracker, analytics, export, calendar, summary, categories) properly scope data by user_id. Version bump: 0.23.1 → 0.23.2 (security patch)
This commit is contained in:
parent
78f95f784e
commit
6b1ef7dcfa
|
|
@ -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
|
### v0.23.1 — Migration Rollback
|
||||||
**Status:** ✅ COMPLETED
|
**Status:** ✅ COMPLETED
|
||||||
**Date:** 2026-05-10
|
**Date:** 2026-05-10
|
||||||
|
|
|
||||||
|
|
@ -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 APP_NAME = 'BillTracker';
|
||||||
|
|
||||||
export const RELEASE_NOTES = {
|
export const RELEASE_NOTES = {
|
||||||
version: '0.23.1',
|
version: '0.23.2',
|
||||||
date: '2026-05-10',
|
date: '2026-05-10',
|
||||||
highlights: [
|
highlights: [
|
||||||
{ icon: '🔄', title: 'Migration Rollback', desc: 'Admin API endpoint to rollback supported migrations with transaction safety and audit logging' },
|
{ 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: 'Migration Logging Enhancement', desc: 'Detailed migration logging with timing for each migration step, error logging with timing, and total migration time reporting' },
|
{ 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.' },
|
||||||
{ 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.' },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bill-tracker",
|
"name": "bill-tracker",
|
||||||
"version": "0.23.1",
|
"version": "0.23.2",
|
||||||
"description": "Monthly bill tracking system",
|
"description": "Monthly bill tracking system",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,9 @@ async function runNotifications() {
|
||||||
const { getCycleRange, resolveDueDate } = require('./statusService');
|
const { getCycleRange, resolveDueDate } = require('./statusService');
|
||||||
const { start, end } = getCycleRange(year, month);
|
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 bills = db.prepare('SELECT * FROM bills WHERE active = 1').all();
|
||||||
const allowUserConfig = getSetting('notify_allow_user_config') === 'true';
|
const allowUserConfig = getSetting('notify_allow_user_config') === 'true';
|
||||||
const globalRecipient = getSetting('notify_global_recipient');
|
const globalRecipient = getSetting('notify_global_recipient');
|
||||||
|
|
@ -223,7 +226,16 @@ async function runNotifications() {
|
||||||
|
|
||||||
if (!type) continue;
|
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) {
|
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
|
// Check recipient's preferences
|
||||||
if (type === 'due_3d' && !recipient.notify_3d) continue;
|
if (type === 'due_3d' && !recipient.notify_3d) continue;
|
||||||
if (type === 'due_1d' && !recipient.notify_1d) continue;
|
if (type === 'due_1d' && !recipient.notify_1d) continue;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue