MyFinances is a client-side personal finance tracking application with strong security practices. This document outlines the security measures implemented and best practices for deployment and usage.
- No Backend Server (default): All data is stored locally in the browser's localStorage
- No Network Transmission: Financial data never leaves your machine
- No API Calls: Eliminates server vulnerabilities
- Offline Capable: Works without internet connection
An opt-in self-hosted Node.js + PostgreSQL backend adds multi-device sync with strong server-side security:
- argon2id password hashing — industry-standard, tuned cost factors
- Server-side sessions — opaque tokens hashed before storage; never stored in plain text
- httpOnly / Secure / SameSite=Strict cookies — session token unreachable from JavaScript
- CSRF double-submit tokens — every mutating request requires a matching
csrfcookie + header pair - Rate limiting — login endpoint protected against brute force
- New-password policy (NIST SP 800-63B rev 4) — 15–128 characters, no composition rules, rejects common, repetitive and context-based passwords on setup, reset and change; optional Have I Been Pwned k-anonymity breach check (
SECURITY_CHECK_BREACHED_PASSWORDS, fails open); changing or resetting the password signs out other sessions. See server/README.md - See DEPLOYMENT.md for setup and server/README.md for architecture
All user inputs are validated and sanitized before processing:
// Removes dangerous characters
// Strips: <, >, ", `, control characters
// Applied to: account names, income names, debt names, etc.// Validates numeric inputs
// Applies min/max bounds
// Prevents NaN and Infinity values
// Used for: amounts, interest rates, balances// Strict ISO date format validation (YYYY-MM-DD)
// Regex: /^\d{4}-\d{2}-\d{2}$/
// Validates date parsing// Converts HTML special characters to entities:
// & → &
// < → <
// > → >
// " → "
// ' → 'All user-provided data displayed in the DOM is HTML-encoded:
// Safe: User data is always escaped before rendering
<span class="acct-card-name">${escapeHtml(a.name)}</span>
// Dangerous pattern (NOT USED in this codebase):
<span>${a.name}</span> // ❌ XSS vulnerabilityProtection Methods:
- Use
escapeHtml()for all user data in innerHTML - Use
textContentinstead of innerHTML where possible - No dynamic script injection
- No eval() usage
- No Function() constructor usage
Strong CSP header implemented in index.html:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://www.googletagmanager.com;
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://cdn.jsdelivr.net https://www.google-analytics.com;
object-src 'none';
base-uri 'self';
form-action 'self'
Note:
frame-ancestors 'none'is intentionally omitted from the meta tag — browsers ignore it there per spec. Addframe-ancestors 'none'to the server-level CSP HTTP header (see DEPLOYMENT.md) alongsideX-Frame-Options: DENYfor clickjacking protection.
Note: The
googletagmanager.com/google-analytics.comorigins support an opt-in, off-by-default Google Analytics integration for self-hosters (GA_MEASUREMENT_IDenv var — see DEPLOYMENT.md's "Google Analytics (Optional)" section). They're allow-listed unconditionally so the feature can work when configured, but nothing loads or is contacted unless you set that variable.
Protection:
- Restricts scripts to self, the trusted Chart.js CDN, and (only if you opt in) Google Analytics
- Prevents inline scripts
- Prevents frame embedding (clickjacking protection)
- Blocks unsafe objects/embeds
- Restricts form submissions
- Size Limit: Maximum 2 MB (prevents memory exhaustion)
- Format Validation: JSON only (via accept attribute)
- Content Validation: Comprehensive sanitization on import
1. File size check (MAX_IMPORT_BYTES = 2 MB)
2. JSON parsing (wrapped in try-catch)
3. Full data validation and re-sanitization
4. Type checking on all fields
5. Invalid records filtered out- Clean JSON export (no sensitive metadata)
- No credentials or tokens included
- User has full control over exported data
- Scope: Same-origin policy enforced by browser
- Encryption: Browser handles encryption at rest
- Isolation: Other origins cannot access data
- Format: Valid JSON only
- ❌ No passwords
- ❌ No API keys
- ❌ No authentication tokens
- ✅ Theme preference (non-sensitive)
- Vanilla JavaScript: No npm packages to audit
- No Third-Party Libraries: Except Chart.js from CDN (for visualization only)
- No Vulnerable Dependencies: All code is self-contained
- Code Review Easy: All source code is readable and auditable
When deploying MyFinances, add these HTTP security headers:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Example for Different Platforms:
app.use((req, res, next) => {
res.set('X-Content-Type-Options', 'nosniff');
res.set('X-Frame-Options', 'DENY');
res.set('X-XSS-Protection', '1; mode=block');
res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});server {
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"$context.Response.AddHeader('X-Content-Type-Options', 'nosniff')
$context.Response.AddHeader('X-Frame-Options', 'DENY')
$context.Response.AddHeader('X-XSS-Protection', '1; mode=block')
$context.Response.AddHeader('Referrer-Policy', 'strict-origin-when-cross-origin')For production deployment:
-
Use HTTPS Only
- Protects data in transit
- Use valid SSL/TLS certificate
- Redirect HTTP to HTTPS
-
Strict-Transport-Security Header
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload -
CORS Configuration
- Only allow same-origin requests
- Avoid
Access-Control-Allow-Origin: *
- Keep browser updated to latest version
- Use a modern browser with CSP support
- Enable JavaScript (required for app)
- Clear browser cache/data when done on shared computers
- Regular Exports: Export data regularly as JSON backup
- Secure Storage: Keep backups in secure locations
- Multiple Copies: Maintain copies on different devices
- Use strong device password/PIN
- Enable device encryption
- Use reputable antivirus software
- Lock device when not in use
- Understand localStorage is readable by any JavaScript
- Only use on trusted devices
- Clear browser data if compromised
- Never share device with untrusted users
The application includes automated security tests:
# Run security test suite
pytest tests/security/ -v
# Tests include:
# - XSS Prevention: HTML/script tag injection
# - CSP Compliance: No inline style or script violations
# - Input Validation: Special characters, bounds
# - Static Analysis: Code patterns, no hardcoded secrets- ✓ XSS attack prevention
- ✓ Input validation and bounds checking
- ✓ Data persistence and restoration
- ✓ File import sanitization
- ✓ No console errors
- ✓ JSON format validation
main is protected by two GitHub rulesets (issue #380, aligned with the BookWheel baseline):
| Ruleset | Enforces | Bypass |
|---|---|---|
| Protect Default Branch - Force Push | No force-pushes, no deletion of main |
None, not even admins |
| Protect Default Branch - Bypass | Every change arrives through a pull request; required checks must pass on an up-to-date branch; CodeQL and Trivy code-scanning block new alerts of high or above; stale reviews are dismissed on push | Admin, pull-request only: an override is possible but is recorded on the PR, and direct pushes are rejected |
- Required checks:
ci-complete: one job inci.ymlthat waits on every CI job and fails if any failed or was cancelled.tests/features/test_ci_shard_coverage.pyfails CI if a job is missing from it.Analyze JavaScript(CodeQL).trivy: blocks fixable CRITICAL/HIGH image vulnerabilities and hardcoded secrets.dependency-review.
- Approvals: the required count is 0. There is one maintainer, and GitHub doesn't allow approving your own PR. The owner reviews each PR before merging.
- Never use the override to merge a red PR. Fix the failure or don't merge.
- Secret scanning and push protection are enabled. Dependabot alerts and security updates are on.
If you discover a security vulnerability:
- Do not create a public GitHub issue
- Do not share vulnerability details publicly
- Contact the repository owner privately
- Include steps to reproduce and impact assessment
- Wait for confirmation before public disclosure
Most Recent Audit: August 31, 2026
- Risk Level: LOW
- Status: PASSED
- No Critical Vulnerabilities: YES
- ✅ All input properly sanitized
- ✅ No XSS vulnerabilities
- ✅ Strong CSP implemented
- ✅ Secure file upload handling
- ✅ No dangerous code patterns
- ✅ Client-side only (no server vulnerabilities)
- ✅ No sensitive data exposure
- ✅ Proper error handling
For full audit reports, see: docs/audit/security/
The following previously planned enhancements are now shipped:
- ✅ Progressive Web App (PWA) —
sw.jsservice worker for offline capability and app-shell caching - ✅ Optional server-side auth — argon2id + session cookies + CSRF for the Postgres backend
Remaining considerations:
- localStorage encryption (browser-side, optional for highly sensitive deployments)
- Password protection for JSON exports
- ✓ OWASP Top 10 Mitigation
- ✓ CWE Coverage (input validation, XSS prevention)
- ✓ CSP Level 3 Implementation
- ✓ No data collection by default
- ✓ No tracking by default — self-hosters may opt their own instance into Google Analytics (see CSP section above)
- ✓ No external API calls unless you enable that opt-in analytics or the optional self-hosted Postgres backend
- ✓ Local storage only (unless the optional Postgres backend is configured)
For security-related questions or concerns, please contact the repository maintainer.
Last Updated: August 31, 2026
Version: 1.1
Status: Production-Ready