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.