Authentication: brute force, password policy and default credentials
Authentication is the mechanism that verifies a user is who they claim to be. It's the first security control of an application, and one of the most frequently poorly implemented. Weaknesses here allow an attacker to access accounts without knowing the password, through brute force, guessing, or exploiting default configurations.
No brute force protection
Without limiting the number of login attempts, an attacker can try thousands of combinations without friction. With a dictionary of common passwords (rockyou.txt contains 14 million entries), it takes just a few minutes to compromise an account with a weak password. On an internet-facing interface, this attack is trivial to automate.
The protections to implement are not mutually exclusive, they combine: temporary lockout after N failures (e.g., 5 attempts, 15-minute block), CAPTCHA after a threshold, progressive delay between attempts, alert to the user on suspicious attempts.
Example: server-side lockout (Node.js / Redis)
const MAX_ATTEMPTS = 5;
const LOCK_DURATION = 15 * 60; // 15 min en secondes
async function checkLoginAttempts(email) {
const key = `login_attempts:${email}`;
const attempts = await redis.incr(key);
if (attempts === 1) await redis.expire(key, LOCK_DURATION);
if (attempts > MAX_ATTEMPTS) {
const ttl = await redis.ttl(key);
throw new Error(`Compte verrouillé. Réessayez dans ${ttl} secondes.`);
}
}
Beware of permanent lockout: a lockout that never expires opens the door to denial of service. An attacker can deliberately lock all accounts by submitting failed attempts. Prefer a temporary or progressive lockout.
Unchanged default credentials
Network equipment, CMS, admin interfaces, and embedded software ship with publicly documented default credentials. admin/admin, admin/password, root/root: these combinations are the first tested by any attacker, and they work far more often than you'd think. Public databases like defaultpassword.de or CIRT.net catalogue default credentials for thousands of products.
The fix is obvious but regularly overlooked: change default credentials on commissioning, ideally force this change on first login. For network equipment, include this point in the deployment checklist.
Insufficient password policy
An insufficient password policy accepts trivial passwords that will be compromised quickly. Common mistakes: minimum length too short (6 or 8 characters), no check against common password dictionaries, arbitrary complexity rules that don't actually improve security ("at least one uppercase"), no reasonable upper limit.
Current NIST recommendations (SP 800-63B) go against received wisdom: prioritise length (at least 8 characters, ideally 12 or more), verify the password doesn't appear in a list of compromised passwords, don't enforce periodic forced rotation (which encourages users to make predictable variations), allow all Unicode characters.
Check against compromised passwords (HaveIBeenPwned API)
// k-anonymity : seuls les 5 premiers caractères du hash SHA-1 sont envoyés
async function isPwnedPassword(password) {
const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const prefix = hash.slice(0, 5);
const suffix = hash.slice(5);
const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const text = await res.text();
return text.split('\n').some(line => line.startsWith(suffix));
}
Plaintext password storage
Storing passwords in plaintext or with an unsalted hash (MD5, SHA-1) is a serious mistake that turns any database compromise into a total account compromise. A database dump with plaintext passwords lets the attacker log into all accounts immediately, including on other services if users reuse passwords.
Generic hashing algorithms (MD5, SHA-256) are not suitable for password storage: they're designed to be fast, which aids brute force attacks. Use algorithms specifically designed for this case: bcrypt, Argon2 (recommended), scrypt.
Secure hashing with Argon2 (Node.js)
const argon2 = require('argon2');
// Lors de la création / modification du mot de passe :
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16, // 64 Mo
timeCost: 3,
parallelism: 1,
});
// Lors de la vérification :
const valid = await argon2.verify(storedHash, password);
Insecure password change mechanism
The password change form must require confirmation of the old password before accepting the new one. Without this check, an attacker who briefly accesses an authenticated session (via XSS, physical access, or an uninvalidated session) can change the password and take permanent control of the account.
After a successful password change: invalidate all other active sessions (except the current one if you want to keep the user logged in), send an email notification, log the event.
Summary
- Limit login attempts: temporary lockout after N failures, progressive delay or CAPTCHA
- Change default credentials: on commissioning, force change on first login
- Length-based password policy: at least 12 characters, check against compromised password lists
- Hash with Argon2 or bcrypt: never MD5, SHA-1, or SHA-256 alone for passwords
- Require old password on change: invalidate other sessions after the change
Further reading
A question after reading?
Doubts about the strength of your authentication? Send a message.
Or book a 30-minute scoping call directly
Book a call →