Skip to main content

Overview

This vulnerability involves storing user passwords in a format that allows the original password to be retrieved by the application or an attacker who gains access to the storage. This includes storing passwords in plaintext (see CWE-312) or using reversible encryption (like AES, DES, two-way encryption) instead of a strong, salted, one-way hash function (like bcrypt, Argon2, PBKDF2, scrypt). 🔑🔄

Business Impact

Storing passwords recoverably is extremely dangerous. If the storage or the encryption key is compromised:
  • Complete Password Exposure: Attackers gain access to the users’ actual passwords, not just hashes.
  • Credential Stuffing: Since users often reuse passwords, attackers can use the exposed passwords to compromise accounts on other unrelated websites.
  • Compliance Violations: Storing passwords reversibly violates numerous security standards and regulations (e.g., PCI-DSS explicitly forbids it).
  • Irreversible Reputational Damage: A breach involving plaintext or easily decrypted passwords causes a massive loss of user trust.
Passwords should never need to be decrypted by the application. Authentication works by hashing the user’s login attempt and comparing it to the stored hash.

Reference Details

CWE ID: CWE-257 OWASP Top 10 (2021): A04:2021 - Insecure Design (Failure to use secure password storage) & A02:2021 - Cryptographic Failures Severity: Critical

Framework-Specific Analysis and Remediation

This is a critical design flaw. The only secure way to store passwords is using a strong, adaptive, salted, one-way hash function. Frameworks provide tools for this (often by default), but developers might mistakenly choose encryption due to misunderstanding or implementing features like “email my password” (which is inherently insecure). Remediation:
  1. Use Hashing ONLY: Replace any encryption logic for passwords with a standard password hashing library (bcrypt, Argon2, PBKDF2, scrypt).
  2. Migrate Existing Passwords: If passwords are currently stored recoverably, implement a migration strategy:
    • Add a new database column for the secure hash.
    • When a user next logs in successfully (using the old, recoverable password), hash the provided password using the new secure method and store it in the new column.
    • Clear the old recoverable password.
    • Eventually, remove the column containing the recoverable passwords.
  3. Eliminate “Recover Password” Features: Replace “email my password” features with secure password reset links that use temporary, single-use, unpredictable tokens.

  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Using cryptography’s Fernet or AES functions to encrypt passwords instead of Django’s password hashers.

Vulnerable Scenario 1: Encrypting Password with Fernet

# models.py (Custom User)
from cryptography.fernet import Fernet
import os
# Assume Fernet key is loaded securely, but the *concept* is flawed
# key = os.environ.get('PASSWORD_ENCRYPTION_KEY')
# f = Fernet(key.encode())

class EncryptedPasswordUser(models.Model):
    username = models.CharField(max_length=150, unique=True)
    # DANGEROUS: Storing encrypted password, recoverable if key leaks.
    encrypted_password = models.BinaryField()

    def set_password(self, raw_password):
        # Encrypting instead of hashing
        self.encrypted_password = f.encrypt(raw_password.encode())

    def check_password(self, raw_password):
        try:
            # Decrypting stored password to compare (INSECURE PATTERN)
            decrypted = f.decrypt(self.encrypted_password).decode()
            return decrypted == raw_password
        except Exception:
            return False

Vulnerable Scenario 2: Storing Plaintext (Covered by CWE-312, but relevant)

# models.py (Custom User)
class PlaintextPasswordUser(models.Model):
    username = models.CharField(max_length=150, unique=True)
    # DANGEROUS: Password stored directly.
    password_cleartext = models.CharField(max_length=128)
    # No hashing occurs

Mitigation and Best Practices

Use Django’s built-in User model or AbstractUser and rely on user.set_password() and user.check_password(), which use configured secure hashers (bcrypt, Argon2, PBKDF2).

Secure Code Example

# models.py (Secure - using AbstractUser)
from django.contrib.auth.models import AbstractUser

class SecureUser(AbstractUser):
    # SECURE: Inherits secure password handling (hashing) from Django.
    # Uses the `password` field managed by Django.
    pass

# Usage:
# user = SecureUser(...)
# user.set_password('raw_password_here') # Hashes the password
# user.save()
# user.check_password('attempted_password') # Compares hash

Testing Strategy

Inspect the database schema and data. Look for columns explicitly storing passwords (password, pwd, secret, etc.). Determine if the stored data is a hash (long, random-looking string, often with prefixes like bcrypt$, argon2$) or encrypted/plaintext (might be Base64, hex, or directly readable). Review code responsible for setting and checking passwords; ensure it uses hashing functions (set_password, check_password, hashers.make_password, hashers.check_password), not encryption/decryption.