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
+8
View File
@@ -262,12 +262,20 @@ function confirmDeposits(db, records, account_id) {
// POST /api/qbo-import/parse
router.post('/parse', upload.single('file'), (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file uploaded.' });
const type = req.body.type;
if (type !== 'checks' && type !== 'deposits') {
fs.unlink(req.file.path, () => {});
return res.status(400).json({ error: 'Invalid type. Must be "checks" or "deposits".' });
}
// Reject non-text MIME types — only CSV/plain text is expected
const mime = (req.file.mimetype || '').toLowerCase();
if (!mime.startsWith('text/') && mime !== 'application/csv' && mime !== 'application/vnd.ms-excel') {
fs.unlink(req.file.path, () => {});
return res.status(400).json({ error: 'File must be a CSV text file.' });
}
let text;
try {
text = fs.readFileSync(req.file.path, 'utf8');