Fix low-level security issues

Content-Security-Policy: add header with default-src 'self',
unsafe-inline for styles (needed for JS-generated inline style attrs),
and data: for embedded logo/signature images.

JSON body limit: reduce from 10mb to 2mb (logo cap is 512KB base64).

Session maxAge: now configurable via SESSION_MAX_AGE_HOURS env var
(default 168h / 7 days). Documented in .env.example.

Password strength: centralize validation in auth.js and raise the bar
to 10+ characters with at least one letter and one non-letter. Applied
consistently to all four password-setting paths (initial setup,
login change-password, admin create user, admin edit user).
This commit is contained in:
2026-03-20 12:25:42 -06:00
parent 2939bfa608
commit bd3e66cd44
4 changed files with 28 additions and 6 deletions
+14 -2
View File
@@ -5,6 +5,15 @@ const router = express.Router();
const bcrypt = require('bcryptjs');
const db = require('../db/database');
// ── Password validation ───────────────────────────────────────────────────────
// Returns an error string if invalid, or null if acceptable.
function validatePassword(password) {
if (!password || password.length < 10) return 'Password must be at least 10 characters.';
if (!/[a-zA-Z]/.test(password)) return 'Password must contain at least one letter.';
if (!/[^a-zA-Z]/.test(password)) return 'Password must contain at least one digit or symbol.';
return null;
}
// ── Login rate limiter ────────────────────────────────────────────────────────
// Tracks failed login attempts per IP. After 10 failures within 15 minutes,
// further attempts are blocked until the window resets.
@@ -57,7 +66,8 @@ router.post('/setup', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) return res.status(400).json({ error: 'Username and password required.' });
if (password.length < 8) return res.status(400).json({ error: 'Password must be at least 8 characters.' });
const pwErr = validatePassword(password);
if (pwErr) return res.status(400).json({ error: pwErr });
const hash = await bcrypt.hash(password, 12);
const result = db.prepare(
@@ -121,7 +131,8 @@ router.post('/change-password', async (req, res) => {
const { current_password, new_password } = req.body;
if (!current_password || !new_password) return res.status(400).json({ error: 'Both fields required.' });
if (new_password.length < 8) return res.status(400).json({ error: 'New password must be at least 8 characters.' });
const pwErr = validatePassword(new_password);
if (pwErr) return res.status(400).json({ error: pwErr });
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.session.userId);
const match = await bcrypt.compare(current_password, user.password_hash);
@@ -135,3 +146,4 @@ router.post('/change-password', async (req, res) => {
});
module.exports = router;
module.exports.validatePassword = validatePassword;