Python MD5 Used for Password Hashing Vulnerability

High Risk Cryptographic Weakness
PythonMD5Password HashingCryptographyAuthenticationHash Function

What it is

Application uses MD5 for password hashing, which is cryptographically broken and vulnerable to rainbow table attacks, collision attacks, and brute force attacks.

import hashlib
from flask import Flask, request

@app.route('/register', methods=['POST'])
def register_user():
    username = request.form.get('username')
    password = request.form.get('password')
    
    # Vulnerable: MD5 without salt
    password_hash = hashlib.md5(password.encode()).hexdigest()
    
    # Store in database
    store_user(username, password_hash)
    return 'User registered'

@app.route('/login', methods=['POST'])
def login_user():
    username = request.form.get('username')
    password = request.form.get('password')
    
    # Vulnerable: MD5 hash comparison
    password_hash = hashlib.md5(password.encode()).hexdigest()
    stored_hash = get_user_hash(username)
    
    if password_hash == stored_hash:
        return 'Login successful'
    return 'Invalid credentials'

# Vulnerable: MD5 with weak salt
def hash_password_weak_salt(password):
    salt = '12345'  # Static salt - very weak
    return hashlib.md5((password + salt).encode()).hexdigest()
import hashlib
import secrets
import bcrypt
from passlib.context import CryptContext
from flask import Flask, request

# Secure password context using multiple strong algorithms
pwd_context = CryptContext(
    schemes=['bcrypt', 'pbkdf2_sha256'],
    default='bcrypt',
    bcrypt__rounds=12,  # Strong work factor
    pbkdf2_sha256__rounds=100000  # High iteration count
)

@app.route('/register', methods=['POST'])
def register_user():
    """Secure user registration with proper password hashing."""
    username = request.form.get('username', '')
    password = request.form.get('password', '')
    
    # Validate input
    if not username or not password:
        return 'Username and password required', 400
    
    # Validate password strength
    if len(password) < 8:
        return 'Password must be at least 8 characters', 400
    
    try:
        # Secure: Use bcrypt with automatic salt generation
        password_hash = pwd_context.hash(password)
        
        # Store in database
        store_user(username, password_hash)
        return 'User registered successfully'
    except Exception as e:
        return 'Registration failed', 500

@app.route('/login', methods=['POST'])
def login_user():
    """Secure user login with proper hash verification."""
    username = request.form.get('username', '')
    password = request.form.get('password', '')
    
    if not username or not password:
        return 'Username and password required', 400
    
    try:
        stored_hash = get_user_hash(username)
        if not stored_hash:
            return 'Invalid credentials', 401
        
        # Secure: Verify using passlib context
        if pwd_context.verify(password, stored_hash):
            # Check if hash needs upgrading
            if pwd_context.needs_update(stored_hash):
                new_hash = pwd_context.hash(password)
                update_user_hash(username, new_hash)
            
            return 'Login successful'
        else:
            return 'Invalid credentials', 401
    except Exception as e:
        return 'Login failed', 500

# Alternative: Manual bcrypt implementation
def secure_hash_password_bcrypt(password):
    """Secure password hashing using bcrypt."""
    if isinstance(password, str):
        password = password.encode('utf-8')
    
    # Generate salt and hash (cost factor 12)
    salt = bcrypt.gensalt(rounds=12)
    return bcrypt.hashpw(password, salt).decode('utf-8')

def verify_password_bcrypt(password, hashed):
    """Verify password against bcrypt hash."""
    if isinstance(password, str):
        password = password.encode('utf-8')
    if isinstance(hashed, str):
        hashed = hashed.encode('utf-8')
    
    return bcrypt.checkpw(password, hashed)

# Alternative: PBKDF2 implementation
def secure_hash_password_pbkdf2(password):
    """Secure password hashing using PBKDF2-SHA256."""
    # Generate random salt
    salt = secrets.token_bytes(32)
    
    # Hash with PBKDF2-SHA256
    key = hashlib.pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt,
        100000  # 100k iterations
    )
    
    # Return salt + hash for storage
    return salt.hex() + ':' + key.hex()

def verify_password_pbkdf2(password, stored_hash):
    """Verify password against PBKDF2 hash."""
    try:
        salt_hex, key_hex = stored_hash.split(':', 1)
        salt = bytes.fromhex(salt_hex)
        stored_key = bytes.fromhex(key_hex)
        
        # Hash provided password with same salt
        key = hashlib.pbkdf2_hmac(
            'sha256',
            password.encode('utf-8'),
            salt,
            100000
        )
        
        # Constant-time comparison
        return secrets.compare_digest(key, stored_key)
    except ValueError:
        return False

# Alternative: Argon2 implementation (requires argon2-cffi)
def secure_hash_password_argon2(password):
    """Secure password hashing using Argon2."""
    try:
        from argon2 import PasswordHasher
        
        ph = PasswordHasher(
            time_cost=3,     # Number of iterations
            memory_cost=65536,  # Memory usage in KB
            parallelism=1,   # Number of threads
            hash_len=32,     # Hash length
            salt_len=16      # Salt length
        )
        
        return ph.hash(password)
    except ImportError:
        raise RuntimeError('argon2-cffi library required for Argon2')

def verify_password_argon2(password, hashed):
    """Verify password against Argon2 hash."""
    try:
        from argon2 import PasswordHasher
        from argon2.exceptions import VerifyMismatchError
        
        ph = PasswordHasher()
        
        try:
            ph.verify(hashed, password)
            return True
        except VerifyMismatchError:
            return False
    except ImportError:
        raise RuntimeError('argon2-cffi library required for Argon2')

# Migration helper for upgrading from MD5
def migrate_md5_hash(username, old_md5_hash, new_password=None):
    """Migrate user from MD5 to secure hash on next login."""
    if new_password:
        # User provided password, upgrade immediately
        new_hash = pwd_context.hash(new_password)
        update_user_hash(username, new_hash)
        return True
    else:
        # Mark for upgrade on next successful login
        mark_user_for_hash_upgrade(username)
        return False

@app.route('/admin/migrate_hashes', methods=['POST'])
def admin_migrate_hashes():
    """Admin endpoint to force hash migration."""
    # This would typically require admin authentication
    if not is_admin(request):
        return 'Unauthorized', 403
    
    users_migrated = 0
    users_marked = 0
    
    for user in get_all_users():
        if is_md5_hash(user['password_hash']):
            # Can't migrate without password, mark for upgrade
            mark_user_for_hash_upgrade(user['username'])
            users_marked += 1
    
    return {
        'users_marked_for_upgrade': users_marked,
        'message': 'Users will be upgraded on next successful login'
    }

# Helper functions
def is_md5_hash(hash_string):
    """Check if hash looks like MD5."""
    return len(hash_string) == 32 and all(c in '0123456789abcdef' for c in hash_string.lower())

def store_user(username, password_hash):
    """Store user in database."""
    # Database implementation
    pass

def get_user_hash(username):
    """Get user hash from database."""
    # Database implementation
    pass

def update_user_hash(username, new_hash):
    """Update user hash in database."""
    # Database implementation
    pass

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Code hashes passwords with MD5: import hashlib; password_hash = hashlib.md5(password.encode()).hexdigest(). MD5 is cryptographically broken. Fast computation enables brute force attacks. GPU cracking tools test billions of hashes per second. Rainbow tables provide pre-computed hashes. MD5 inappropriate for passwords.

Root causes

Using MD5 for Password Hashing

Code hashes passwords with MD5: import hashlib; password_hash = hashlib.md5(password.encode()).hexdigest(). MD5 is cryptographically broken. Fast computation enables brute force attacks. GPU cracking tools test billions of hashes per second. Rainbow tables provide pre-computed hashes. MD5 inappropriate for passwords.

Legacy Systems with MD5 Password Storage

Old databases storing MD5 hashes. Migration from legacy systems without password rehashing. Backward compatibility with MD5 authentication. Inherited codebases using MD5. Legacy password storage persisting in modern applications. Historical MD5 usage creates ongoing vulnerability.

Using MD5 Without Salt

Unsalted MD5 hashing: md5(password). Same password produces same hash across users. Rainbow table attacks effective. Identical passwords identifiable in database. No unique per-user randomness. Unsalted hashes vulnerable to pre-computation attacks. Combined with MD5 weakness, provides minimal security.

Misunderstanding MD5 as Sufficient for Security

Developers believing hashing alone sufficient. Not understanding MD5 speed enables attacks. Assuming one-way function provides security. Missing awareness of modern password hashing requirements. Confusion between hashing for integrity (checksums) versus password storage. MD5 appropriate for checksums, not passwords.

Using MD5 with Insufficient Iterations

Iterating MD5 for key stretching: for i in range(1000): h = md5(h). Insufficient iterations for modern attacks. GPU computing makes even 10,000 iterations inadequate. MD5 speed compounds iteration insufficiency. Modern password hashing requires millions of iterations or memory-hard algorithms.

Fixes

1

Use bcrypt for Password Hashing

Replace MD5 with bcrypt: import bcrypt; hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)). Designed for passwords. Configurable work factor. Automatic salting. Resistant to GPU attacks. Industry standard. Use rounds=12 or higher. Verification: bcrypt.checkpw(password.encode(), hashed).

2

Use Argon2 for Modern Password Hashing

Use Argon2id: from argon2 import PasswordHasher; ph = PasswordHasher(); hash = ph.hash(password). Winner of Password Hashing Competition. Memory-hard algorithm. Resistant to GPU and ASIC attacks. Configurable memory, time, parallelism. Best choice for new applications. Verification: ph.verify(hash, password).

3

Use PBKDF2 with High Iteration Count

If bcrypt/Argon2 unavailable, use PBKDF2: from hashlib import pbkdf2_hmac; hash = pbkdf2_hmac('sha256', password.encode(), salt, 600000). Minimum 600,000 iterations (OWASP 2023). Random salt per password. SHA-256 or SHA-512. Better than MD5 but prefer bcrypt/Argon2.

4

Implement Password Rehashing on Login

Migrate legacy MD5: on login, if verify_md5(password, stored_hash): new_hash = bcrypt.hashpw(password); update_user_hash(new_hash). Gradually migrate users. Next login gets secure hash. Opportunistic upgrade. Complete migration over time. Improves security without forcing password resets.

5

Use Django or Flask Security Utilities

Framework password hashing: Django uses PBKDF2 by default: from django.contrib.auth.hashers import make_password, check_password. Flask with Werkzeug: from werkzeug.security import generate_password_hash, check_password_hash. Frameworks handle algorithm selection, salting, iterations. Use framework utilities instead of manual hashing.

6

Implement Password Policy and Breach Detection

Add security layers: minimum length 12 characters. Check against HaveIBeenPwned database. Prevent common passwords. Multi-factor authentication. Strong password hashing important but one layer. Defense-in-depth with multiple security measures. Password policies reduce brute force feasibility.

Detect This Vulnerability in Your Code

Sourcery automatically identifies python md5 used for password hashing vulnerability and many other security issues in your codebase.