Every app has security vulnerabilities. The difference between responsible teams and irresponsible ones is how they handle them. In December 2025, we conducted a comprehensive security audit of Simple Money Tracker. What we found was humbling. What we fixed made the app fundamentally safer.
This post is a transparent account of what went wrong, how we fixed it, and what we changed to prevent recurrence. Because security by obscurity isn't security at all.
Vulnerability #1: Broken Access Control (IDOR)
Insecure Direct Object Reference (IDOR) is a deceptively simple attack: if an API endpoint accepts a resource ID but doesn't verify that the requesting user owns that resource, an attacker can access any user's data by cycling through IDs.
We found this pattern in several server action endpoints. An endpoint that deleted a debt accepted `debtId` but only verified the user was authenticated — not that the debt belonged to them. An attacker could delete anyone's debts. We fixed this by adding explicit ownership verification to every single endpoint: before any mutation, the server fetches the target document, checks that its `userId` field matches the authenticated user, and rejects the request if it doesn't.
Vulnerability #2: Business Logic Flaw in Payments
Our Pro subscription system had a race condition: the activation check and the status update were two separate operations with no transactional guarantee. In theory, a user could activate a payment link multiple times in rapid succession and receive multiple subscription periods for the price of one.
We fixed this by wrapping the entire payment activation flow in a Firestore transaction: read the payment status, check if already consumed, update the subscription, mark the payment as used — all atomically. No gap between check and write means no exploit possible.
Vulnerability #3: Open API Endpoints
Several API routes were missing token verification entirely. They assumed authentication was handled by middleware, but the middleware wasn't applied consistently. The result: endpoints that accepted unauthenticated requests, processing data without knowing who the requester was.
The fix was systematic: we audited every API route, added strict ID token verification to each one, and implemented a centralized validation utility that's imported by all routes. No route processes data without identity verification. Period.
Vulnerability #4: XSS Vectors
User-generated content — expense descriptions, note text, message bodies — was being rendered without proper sanitization in some contexts. An attacker could inject script tags or HTML into their expense description, and when another user viewed that expense in a shared group, the script would execute.
We addressed this with two layers: server-side validation (rejecting any input containing script tags or event handlers) and client-side sanitization using DOMPurify before rendering any user-generated content. Defense in depth — because one layer is never enough.
Security is not a feature. It's a process.
We now run automated security scanning on every PR, enforce strict Zod schema validation on all inputs, and conduct quarterly manual audits. Never again will an endpoint go live without ownership verification.
What Changed: Prevention Over Patching
The most important outcome wasn't fixing the four vulnerabilities — it was institutionalizing the practices that prevent them. We introduced mandatory Zod schema validation on all server actions and API routes. We implemented strict Firestore security rules with a whitelist approach. We added input length limits to prevent text-based abuse. We updated Firebase Storage rules to only allow safe file types.
Every new endpoint now includes an explicit authorization check as part of its boilerplate. Every PR is reviewed against a security checklist. The vulnerabilities we found made us better engineers — and Simple Money Tracker a safer place for user data.
