Weak Password Controls
Weak Password Controls at a glance
Overview
Weak password controls allow users to choose passwords that are trivial to guess or already compromised. Without length requirements, common-password lists, and breach checks, attackers succeed with basic guessing and stuffing. Low-cost hashing makes offline cracking easy after a leak.
Where it occurs
It occurs in password validation and storage systems that use weak complexity rules, short length requirements, disabled checks, or poorly configured hashing cost factors.
Impact
Accounts are easily taken over through low-effort attacks. If password databases are exposed, weak hashing parameters enable rapid recovery of many user passwords.
Prevention
Prevent this by enforcing strong, length-first password policies, blocking breached or common passwords, hashing with modern KDFs (bcrypt, scrypt, Argon2), and combining with rate limiting and MFA for robust authentication security.
Examples
Switch tabs to view language/framework variants.
Registration accepts extremely weak passwords
No minimum length or common-password checks. Users can set passwords like '1234'.
app.post('/register', async (req,res)=>{
const { email, password } = req.body;
// BUG: no strength checks at all
const hash = await bcrypt.hash(password, 10);
await Users.insert({ email, hash });
res.send('ok');
});- Line 3: No minimum length or composition checks
Allowing trivial passwords makes guessing and stuffing succeed.
app.post('/register', async (req,res)=>{
const { email, password } = req.body;
if (typeof password !== 'string') return res.status(400).send('bad');
if (password.length < 12) return res.status(400).send('too short');
if (/\s/.test(password)) return res.status(400).send('no spaces');
if (COMMON_PASSWORDS.has(password.toLowerCase())) return res.status(400).send('too common');
const hash = await bcrypt.hash(password, 12);
await Users.insert({ email, hash });
res.send('ok');
});- Line 6: Rejects common passwords and enforces length
Require length, reject common passwords, and use a strong hash cost.
Engineer Checklist
-
Set minimum length to at least 12 characters
-
Reject common and known-breached passwords
-
Use bcrypt/Argon2 with cost tuned for ~100 ms
-
Add signup and password-change validation, not just login checks
-
Pair with rate limiting, login risk checks, and MFA
End-to-End Example
A registration route accepts 'password123' and stores it with a low-cost hash. Attackers guess and reuse such passwords easily.
// Node.js/Express + bcrypt - Vulnerable weak password controls
const bcrypt = require('bcrypt');
app.post('/api/register', async (req, res) => {
try {
const { email, password } = req.body;
// VULNERABLE: No password validation whatsoever!
// Accepts: "123", "password", "admin", "qwerty", etc.
// No minimum length check
// No complexity requirements
// No common password blocking
// No breach check
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already registered' });
}
// VULNERABLE: Extremely low bcrypt cost (4 rounds)
// This makes offline cracking trivial if database leaks
// Recommended: 10-12 rounds minimum
const saltRounds = 4; // DANGEROUS!
const hashedPassword = await bcrypt.hash(password, saltRounds);
const newUser = await User.create({
email,
password: hashedPassword
});
res.json({ message: 'Registration successful', userId: newUser._id });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// ALSO VULNERABLE: Password change with no validation
app.post('/api/change-password', authenticateToken, async (req, res) => {
const { currentPassword, newPassword } = req.body;
const userId = req.user.id;
const user = await User.findById(userId);
const validCurrent = await bcrypt.compare(currentPassword, user.password);
if (!validCurrent) {
return res.status(401).json({ error: 'Current password incorrect' });
}
// VULNERABLE: Accepts ANY new password, even "1" or "password"
const hashedNew = await bcrypt.hash(newPassword, 4);
await User.findByIdAndUpdate(userId, { password: hashedNew });
res.json({ message: 'Password changed successfully' });
});// Node.js/Express + bcrypt - SECURE password controls
const bcrypt = require('bcrypt');
// Common password list (top 10k most common passwords)
const COMMON_PASSWORDS = new Set([
'password', '123456', '12345678', 'qwerty', 'abc123',
'monkey', '1234567', 'letmein', 'trustno1', 'dragon',
'baseball', '111111', 'iloveyou', 'master', 'sunshine',
// ... load from file or API
]);
function validatePassword(password) {
const errors = [];
// Minimum length: 12 characters
if (password.length < 12) {
errors.push('Password must be at least 12 characters long');
}
// Maximum length to prevent DoS
if (password.length > 128) {
errors.push('Password must be less than 128 characters');
}
// Check against common passwords
if (COMMON_PASSWORDS.has(password.toLowerCase())) {
errors.push('This password is too common. Please choose a different one.');
}
// Optional: Check for sequential patterns
if (/^(\d)\1+$/.test(password) || /^(abc|123|qwe)/i.test(password)) {
errors.push('Password contains sequential patterns');
}
return errors;
}
// Optional: Check against HaveIBeenPwned API (k-anonymity)
async function checkBreachedPassword(password) {
const crypto = require('crypto');
const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const prefix = hash.substring(0, 5);
const suffix = hash.substring(5);
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const text = await response.text();
return text.includes(suffix);
}
app.post('/api/register', async (req, res) => {
try {
const { email, password } = req.body;
// SECURE: Validate password strength
const validationErrors = validatePassword(password);
if (validationErrors.length > 0) {
return res.status(400).json({ errors: validationErrors });
}
// SECURE: Check if password has been breached
const isBreached = await checkBreachedPassword(password);
if (isBreached) {
return res.status(400).json({
error: 'This password has been exposed in a data breach. Please choose a different password.'
});
}
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already registered' });
}
// SECURE: Use strong bcrypt cost (12 rounds = ~250ms)
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
const newUser = await User.create({
email,
password: hashedPassword
});
res.json({ message: 'Registration successful', userId: newUser._id });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
app.post('/api/change-password', authenticateToken, async (req, res) => {
const { currentPassword, newPassword } = req.body;
const userId = req.user.id;
const user = await User.findById(userId);
const validCurrent = await bcrypt.compare(currentPassword, user.password);
if (!validCurrent) {
return res.status(401).json({ error: 'Current password incorrect' });
}
// SECURE: Validate new password
const validationErrors = validatePassword(newPassword);
if (validationErrors.length > 0) {
return res.status(400).json({ errors: validationErrors });
}
// Check breach status
const isBreached = await checkBreachedPassword(newPassword);
if (isBreached) {
return res.status(400).json({
error: 'This password has been exposed in a data breach.'
});
}
const hashedNew = await bcrypt.hash(newPassword, 12);
await User.findByIdAndUpdate(userId, { password: hashedNew });
res.json({ message: 'Password changed successfully' });
});Discovery
This vulnerability is discovered by attempting to set or change passwords using weak values (like '123456', 'password', or single characters) and observing that the application accepts them without enforcing minimum complexity, length, or common password checks.
-
1. Test minimum length requirement
httpAction
Attempt registration with very short password
Request
POST https://app.example.com/api/registerHeaders:Content-Type: application/jsonBody:{ "email": "test1@example.com", "password": "123" }Response
Status: 200Body:{ "message": "Account created successfully", "user_id": "user_001", "note": "3-character password accepted - no minimum length enforced!" }Artifacts
account_created no_length_validation weak_policy_confirmed -
2. Test common password acceptance
httpAction
Register with top common passwords
Request
POST https://app.example.com/api/registerHeaders:Content-Type: application/jsonBody:{ "email": "test2@example.com", "password": "password123" }Response
Status: 200Body:{ "message": "Registration successful", "user_id": "user_002", "note": "Common password 'password123' accepted without breach check" }Artifacts
common_password_accepted no_dictionary_check no_breach_validation -
3. Test password complexity validation
httpAction
Try repetitive or pattern-based passwords
Request
POST https://app.example.com/api/registerHeaders:Content-Type: application/jsonBody:{ "email": "test3@example.com", "password": "aaaaaaaa" }Response
Status: 200Body:{ "message": "Account created", "user_id": "user_003", "note": "Repetitive password accepted - no entropy checks" }Artifacts
repetitive_password_accepted no_complexity_enforcement weak_entropy_allowed
Exploit steps
An attacker exploits this by conducting more effective brute force or dictionary attacks against accounts with weak passwords, successfully compromising accounts at scale, especially when combined with credential stuffing from breached password databases.
-
1. Credential stuffing attack
Login attempts with common passwords
httpAction
Use top 100 common passwords against user accounts
Request
POST https://app.example.com/api/loginHeaders:Content-Type: application/jsonBody:{ "email": "victim1@example.com", "password": "password123" }Response
Status: 200Body:{ "message": "Login successful", "user": { "id": "user_456", "email": "victim1@example.com" }, "session_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "stats": "47 out of 500 tested accounts compromised using top 100 passwords" }Artifacts
accounts_compromised successful_stuffing weak_password_exploitation -
2. Password spraying campaign
Single password across many accounts
httpAction
Try one common password per account to evade rate limits
Request
POST https://app.example.com/api/loginHeaders:Content-Type: application/jsonBody:{ "email": "[rotate through user list]", "password": "Summer2024!" }Response
Status: 200Body:{ "campaign_results": { "accounts_tested": 1000, "successful_logins": 73, "success_rate": "7.3%", "detection_avoided": true }, "note": "Slow spraying bypasses rate limits, many weak passwords discovered" }Artifacts
spray_campaign_success rate_limit_evasion multiple_compromises -
3. Offline hash cracking
Crack leaked password hashes
cliAction
Extract passwords from leaked database using hashcat
Request
Response
Artifacts
passwords_cracked plaintext_recovered weak_kdf_exploited -
4. Privilege escalation via admin account
Access compromised admin account
httpAction
Login to admin account using cracked weak password
Request
POST https://app.example.com/api/loginHeaders:Content-Type: application/jsonBody:{ "email": "admin@company.com", "password": "Admin123!" }Response
Status: 200Body:{ "message": "Admin login successful", "user": { "id": "admin_1", "email": "admin@company.com", "role": "administrator", "permissions": [ "*" ] }, "admin_panel": "https://app.example.com/admin", "note": "Full system access via weak admin password" }Artifacts
admin_access_granted privilege_escalation full_system_compromise
Specific Impact
Multiple accounts are compromised cheaply, leading to data exposure and support load.
If hashes leak, offline cracking recovers many passwords due to low KDF cost.
Fix
Implement a length-first policy, reject common and breached passwords, and store with a strong KDF. Tune hashing cost for your hardware and add MFA and rate limiting to reduce risk from guessing.
Detect This Vulnerability in Your Code
Sourcery automatically identifies weak password controls vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free