Weak Password Controls

Weak Password PolicyPoor Password Hygiene

Weak Password Controls at a glance

What it is: The application lets users set weak or breached passwords, or stores them with inadequate hashing parameters.
Why it happens: Weak password policies occur when validation is minimal, relying only on composition or short length rules, and when hashing functions like bcrypt use insufficient cost factors.
How to fix: Enforce strong passphrases (12+ characters), block common or breached passwords, and use modern hashing with proper cost factors and authentication rate limiting.

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.

sequenceDiagram participant Browser participant App as App Server participant DB as Auth DB Browser->>App: POST /register password=password123 App->>DB: Store weak hash (low cost) DB-->>App: ok Browser->>App: POST /login (stuffing) App-->>Browser: Success for many accounts note over App,DB: Enforce length, common/breached checks, and strong KDF cost
A potential flow for a Weak Password Controls exploit

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'.

Vulnerable
JavaScript • Express — Bad
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.

Secure
JavaScript • Express — Good
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.

Vulnerable
JAVASCRIPT
// 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' });
});
Secure
JAVASCRIPT
// 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. 1. Test minimum length requirement

    http

    Action

    Attempt registration with very short password

    Request

    POST https://app.example.com/api/register
    Headers:
    Content-Type: application/json
    Body:
    {
      "email": "test1@example.com",
      "password": "123"
    }

    Response

    Status: 200
    Body:
    {
      "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. 2. Test common password acceptance

    http

    Action

    Register with top common passwords

    Request

    POST https://app.example.com/api/register
    Headers:
    Content-Type: application/json
    Body:
    {
      "email": "test2@example.com",
      "password": "password123"
    }

    Response

    Status: 200
    Body:
    {
      "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. 3. Test password complexity validation

    http

    Action

    Try repetitive or pattern-based passwords

    Request

    POST https://app.example.com/api/register
    Headers:
    Content-Type: application/json
    Body:
    {
      "email": "test3@example.com",
      "password": "aaaaaaaa"
    }

    Response

    Status: 200
    Body:
    {
      "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. 1. Credential stuffing attack

    Login attempts with common passwords

    http

    Action

    Use top 100 common passwords against user accounts

    Request

    POST https://app.example.com/api/login
    Headers:
    Content-Type: application/json
    Body:
    {
      "email": "victim1@example.com",
      "password": "password123"
    }

    Response

    Status: 200
    Body:
    {
      "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. 2. Password spraying campaign

    Single password across many accounts

    http

    Action

    Try one common password per account to evade rate limits

    Request

    POST https://app.example.com/api/login
    Headers:
    Content-Type: application/json
    Body:
    {
      "email": "[rotate through user list]",
      "password": "Summer2024!"
    }

    Response

    Status: 200
    Body:
    {
      "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. 3. Offline hash cracking

    Crack leaked password hashes

    cli

    Action

    Extract passwords from leaked database using hashcat

    Request

    Response

    Artifacts

    passwords_cracked plaintext_recovered weak_kdf_exploited
  4. 4. Privilege escalation via admin account

    Access compromised admin account

    http

    Action

    Login to admin account using cracked weak password

    Request

    POST https://app.example.com/api/login
    Headers:
    Content-Type: application/json
    Body:
    {
      "email": "admin@company.com",
      "password": "Admin123!"
    }

    Response

    Status: 200
    Body:
    {
      "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