fix: enforce consistent password policy across all auth flows
Replace duplicated inline validation with a shared validatePassword() utility that checks minimum length (8), rejects repetitive and common passwords, and requires uppercase, lowercase, a digit, and a special character. - Add server/src/services/passwordPolicy.ts as single source of truth - Apply to registration, password change, and admin create/edit user (admin routes previously had zero validation) - Fix client min-length mismatch (6 vs 8) in RegisterPage and LoginPage - Add client-side password length guard to AdminPage forms - Update register.passwordTooShort and settings.passwordWeak i18n keys in all 12 locales to reflect the corrected requirements
This commit is contained in:
@@ -10,6 +10,7 @@ import { writeAudit, getClientIp, logInfo } from '../services/auditLog';
|
||||
import { getAllPermissions, savePermissions, PERMISSION_ACTIONS } from '../services/permissions';
|
||||
import { revokeUserSessions } from '../mcp';
|
||||
import { maybe_encrypt_api_key, decrypt_api_key } from '../services/apiKeyCrypto';
|
||||
import { validatePassword } from '../services/passwordPolicy';
|
||||
import { updateJwtSecret } from '../config';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -47,6 +48,9 @@ router.post('/users', (req: Request, res: Response) => {
|
||||
return res.status(400).json({ error: 'Username, email and password are required' });
|
||||
}
|
||||
|
||||
const pwCheck = validatePassword(password.trim());
|
||||
if (!pwCheck.ok) return res.status(400).json({ error: pwCheck.reason });
|
||||
|
||||
if (role && !['user', 'admin'].includes(role)) {
|
||||
return res.status(400).json({ error: 'Invalid role' });
|
||||
}
|
||||
@@ -97,6 +101,10 @@ router.put('/users/:id', (req: Request, res: Response) => {
|
||||
if (conflict) return res.status(409).json({ error: 'Email already taken' });
|
||||
}
|
||||
|
||||
if (password) {
|
||||
const pwCheck = validatePassword(password);
|
||||
if (!pwCheck.ok) return res.status(400).json({ error: pwCheck.reason });
|
||||
}
|
||||
const passwordHash = password ? bcrypt.hashSync(password, 12) : null;
|
||||
|
||||
db.prepare(`
|
||||
|
||||
@@ -10,6 +10,7 @@ import fetch from 'node-fetch';
|
||||
import { authenticator } from 'otplib';
|
||||
import QRCode from 'qrcode';
|
||||
import { db } from '../db/database';
|
||||
import { validatePassword } from '../services/passwordPolicy';
|
||||
import { authenticate, optionalAuth, demoUploadBlock } from '../middleware/auth';
|
||||
import { JWT_SECRET } from '../config';
|
||||
import { encryptMfaSecret, decryptMfaSecret } from '../services/mfaCrypto';
|
||||
@@ -268,13 +269,8 @@ router.post('/register', authLimiter, (req: Request, res: Response) => {
|
||||
return res.status(400).json({ error: 'Username, email and password are required' });
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/[0-9]/.test(password)) {
|
||||
return res.status(400).json({ error: 'Password must contain at least one uppercase letter, one lowercase letter, and one number' });
|
||||
}
|
||||
const pwCheck = validatePassword(password);
|
||||
if (!pwCheck.ok) return res.status(400).json({ error: pwCheck.reason });
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
@@ -382,11 +378,8 @@ router.put('/me/password', authenticate, rateLimiter(5, RATE_LIMIT_WINDOW), (req
|
||||
const { current_password, new_password } = req.body;
|
||||
if (!current_password) return res.status(400).json({ error: 'Current password is required' });
|
||||
if (!new_password) return res.status(400).json({ error: 'New password is required' });
|
||||
if (new_password.length < 8) return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
||||
|
||||
if (!/[A-Z]/.test(new_password) || !/[a-z]/.test(new_password) || !/[0-9]/.test(new_password)) {
|
||||
return res.status(400).json({ error: 'Password must contain at least one uppercase letter, one lowercase letter, and one number' });
|
||||
}
|
||||
const pwCheck = validatePassword(new_password);
|
||||
if (!pwCheck.ok) return res.status(400).json({ error: pwCheck.reason });
|
||||
|
||||
const user = db.prepare('SELECT password_hash FROM users WHERE id = ?').get(authReq.user.id) as { password_hash: string } | undefined;
|
||||
if (!user || !bcrypt.compareSync(current_password, user.password_hash)) {
|
||||
|
||||
27
server/src/services/passwordPolicy.ts
Normal file
27
server/src/services/passwordPolicy.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
const COMMON_PASSWORDS = new Set([
|
||||
'password', '12345678', '123456789', '1234567890', 'password1',
|
||||
'qwerty123', 'iloveyou', 'admin123', 'letmein12', 'welcome1',
|
||||
'monkey123', 'dragon12', 'master12', 'qwerty12', 'abc12345',
|
||||
'trustno1', 'baseball', 'football', 'shadow12', 'michael1',
|
||||
'jennifer', 'superman', 'abcdefgh', 'abcd1234', 'password123',
|
||||
'admin1234', 'changeme', 'welcome123', 'passw0rd', 'p@ssword',
|
||||
]);
|
||||
|
||||
export function validatePassword(password: string): { ok: boolean; reason?: string } {
|
||||
if (password.length < 8) return { ok: false, reason: 'Password must be at least 8 characters' };
|
||||
|
||||
if (/^(.)\1+$/.test(password)) {
|
||||
return { ok: false, reason: 'Password is too repetitive' };
|
||||
}
|
||||
|
||||
if (COMMON_PASSWORDS.has(password.toLowerCase())) {
|
||||
return { ok: false, reason: 'Password is too common. Please choose a unique password.' };
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password)) return { ok: false, reason: 'Password must contain at least one uppercase letter' };
|
||||
if (!/[a-z]/.test(password)) return { ok: false, reason: 'Password must contain at least one lowercase letter' };
|
||||
if (!/[0-9]/.test(password)) return { ok: false, reason: 'Password must contain at least one number' };
|
||||
if (!/[^A-Za-z0-9]/.test(password)) return { ok: false, reason: 'Password must contain at least one special character' };
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
Reference in New Issue
Block a user