feat: enforce maximum password length of 64 characters in user registration and password update
Build & Push Docker Image / build (push) Successful in 4m19s
Build & Push Docker Image / build (push) Successful in 4m19s
This commit is contained in:
+25
-1
@@ -43,6 +43,13 @@ const SAFE_ID_RE = /^[a-zA-Z0-9_-]{1,50}$/;
|
||||
const SAFE_COLOR_RE = /^(?:#[0-9a-fA-F]{3,8}|hsl\(\d{1,3},\s*\d{1,3}%,\s*\d{1,3}%\)|[a-zA-Z]{1,30})$/;
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 8;
|
||||
// bcrypt only uses the first 72 bytes; cap input to prevent CPU-DoS on hashing.
|
||||
const MAX_PASSWORD_LENGTH = 64;
|
||||
|
||||
// Pre-computed bcrypt hash of a random string used as a dummy comparison
|
||||
// target when the requested account does not exist. Keeps login timing
|
||||
// roughly constant so we do not leak whether an email is registered.
|
||||
const DUMMY_BCRYPT_HASH = bcrypt.hashSync('dummy-password-for-timing-' + Math.random(), 12);
|
||||
|
||||
// ── Rate Limiters ────────────────────────────────────────────────────────────
|
||||
const loginLimiter = rateLimit({
|
||||
@@ -168,6 +175,9 @@ router.post('/register', registerLimiter, async (req, res) => {
|
||||
if (password.length < MIN_PASSWORD_LENGTH) {
|
||||
return res.status(400).json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters long` });
|
||||
}
|
||||
if (password.length > MAX_PASSWORD_LENGTH) {
|
||||
return res.status(400).json({ error: `Password must not exceed ${MAX_PASSWORD_LENGTH} characters` });
|
||||
}
|
||||
|
||||
const existing = await db.get('SELECT id FROM users WHERE email = ?', [email]);
|
||||
if (existing) {
|
||||
@@ -351,10 +361,18 @@ router.post('/login', loginLimiter, async (req, res) => {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Cap password length to keep bcrypt CPU work bounded.
|
||||
if (typeof password !== 'string' || password.length > MAX_PASSWORD_LENGTH) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const user = await db.get('SELECT * FROM users WHERE email = ?', [email.toLowerCase()]);
|
||||
|
||||
if (!user || !bcrypt.compareSync(password, user.password_hash)) {
|
||||
// Always run bcrypt against either the real hash or a dummy hash so login
|
||||
// timing does not reveal whether the email is registered.
|
||||
const passwordOk = bcrypt.compareSync(password, user?.password_hash || DUMMY_BCRYPT_HASH);
|
||||
if (!user || !passwordOk) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
@@ -560,6 +578,9 @@ router.put('/password', authenticateToken, passwordLimiter, async (req, res) =>
|
||||
if (typeof currentPassword !== 'string' || typeof newPassword !== 'string') {
|
||||
return res.status(400).json({ error: 'Invalid input' });
|
||||
}
|
||||
if (currentPassword.length > MAX_PASSWORD_LENGTH || newPassword.length > MAX_PASSWORD_LENGTH) {
|
||||
return res.status(400).json({ error: `Password must not exceed ${MAX_PASSWORD_LENGTH} characters` });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
@@ -823,6 +844,9 @@ router.post('/2fa/disable', authenticateToken, twoFaLimiter, async (req, res) =>
|
||||
if (!password || !code) {
|
||||
return res.status(400).json({ error: 'Password and code are required' });
|
||||
}
|
||||
if (typeof password !== 'string' || password.length > MAX_PASSWORD_LENGTH) {
|
||||
return res.status(400).json({ error: 'Invalid password' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const user = await db.get('SELECT password_hash, totp_secret, totp_enabled FROM users WHERE id = ?', [req.user.id]);
|
||||
|
||||
Reference in New Issue
Block a user