Fix high and medium security vulnerabilities

CSRF: upgrade session cookie sameSite from 'lax' to 'strict'.

Rate limiting: login endpoint now blocks an IP after 10 failed attempts
in a 15-minute window; resets on success. In-memory, no new dependency.

SESSION_SECRET: server exits at startup when NODE_ENV=production and
SESSION_SECRET is unset. docker-compose.yml updated to pass it via env;
.env.example added with generation instructions.

Security headers: add X-Content-Type-Options, X-Frame-Options, and
Referrer-Policy to all responses.

Sensitive data: routing_number and account_number are now omitted from
GET /api/account/:id responses for non-admin users.

Image size: logo upload capped at 512 KB in the account PUT handler.

Amount validation: checks (POST/PUT) and deposit items (POST/PUT) now
reject non-finite and non-positive amounts.

QBO import: uploaded file is rejected if its MIME type is not text or
a known CSV variant.
This commit is contained in:
2026-03-20 08:21:23 -06:00
parent 0f00624e61
commit 2939bfa608
8 changed files with 127 additions and 13 deletions
+18 -2
View File
@@ -49,6 +49,14 @@ router.post('/', (req, res) => {
if (!isEditorForAccount(req.session, parseInt(account_id, 10))) {
return res.status(403).json({ error: 'Write access required.' });
}
if (Array.isArray(items)) {
for (const item of items) {
const a = parseFloat(item.amount);
if (!isFinite(a) || a <= 0) {
return res.status(400).json({ error: 'Each deposit item amount must be a positive number.' });
}
}
}
const insert = db.transaction(() => {
const result = db.prepare(`
@@ -76,7 +84,7 @@ router.post('/', (req, res) => {
item.bank_no || null,
item.payee || null,
item.memo || null,
parseFloat(item.amount) || 0,
parseFloat(item.amount),
);
});
}
@@ -98,6 +106,14 @@ router.put('/:id', (req, res) => {
const { deposit_date, currency, coin, cash_back, items } = req.body;
if (!deposit_date) return res.status(400).json({ error: 'deposit_date is required.' });
if (Array.isArray(items)) {
for (const item of items) {
const a = parseFloat(item.amount);
if (!isFinite(a) || a <= 0) {
return res.status(400).json({ error: 'Each deposit item amount must be a positive number.' });
}
}
}
const update = db.transaction(() => {
db.prepare(`
@@ -124,7 +140,7 @@ router.put('/:id', (req, res) => {
item.bank_no || null,
item.payee || null,
item.memo || null,
parseFloat(item.amount) || 0,
parseFloat(item.amount),
);
});
}