CWE-256: Plaintext Storage of a Password
Overview
Plaintext storage of a password means a credential is written to disk, memory, logs, or a database column with no one-way transformation applied at all - not weak encoding, not fast hashing, nothing. Anyone who reads that storage location (a database dump, a config file, a log line, a memory snapshot) recovers the password directly.
Relationship to Other CWEs
CWE-256 is a child of CWE-522 (Insufficiently Protected Credentials), the general weakness of credentials without adequate protection. MITRE also files it alongside CWE-916 under the "Credentials Management Errors" category (CWE-255), which is part of why the two are confused in scan findings.
Several CWEs get reported against the same password column. Which one applies depends on what the column actually holds:
- CWE-256 (this page) - the column holds the password itself, with no transformation applied at all.
- CWE-916 (Use of Password Hash With Insufficient Computational Effort) - a password hash that exists but uses an algorithm or cost factor too fast to resist offline cracking. See that page's own
Relationship to Other CWEsfor its narrower salting children, CWE-759 and CWE-760. - CWE-261 (Weak Encoding for Password) - a reversible encoding such as Base64, hex or ROT13 applied to the password and mistaken for protection. The stored value is not the password verbatim, and that is the only difference to the risk: it is recoverable by anyone who can read it. The destination is the same one-way hash, but the migration is not: an encoded column has to be decoded before it is hashed, where this page's plaintext column can be hashed in place. Follow CWE-261's procedure for one of those findings rather than this page's.
- CWE-257 (Storing Passwords in a Recoverable Format) - passwords stored encrypted but reversible, recoverable by whoever holds the key. There is no page here for it yet. That is a step above plaintext but still allows the original password to be recovered rather than only verified. MITRE lists no formal relationship between it and CWE-256.
- CWE-798 (Use of Hard-coded Credentials) / CWE-259 (Use of Hard-coded Password) - a secret embedded directly in source code or config, not how a password field is stored once collected from a user. MITRE lists no relationship between CWE-256 and either.
OWASP Classification
A06:2025 - Insecure Design
Risk
Critical: The moment the storage is breached, every stored password is exposed at once and every account is compromised, with no cracking effort required. Because people reuse passwords, the same list feeds credential stuffing against other sites. Plaintext password storage can violate applicable PCI DSS requirements or security obligations under GDPR or HIPAA. Which requirements apply depends on the organisation, system and data involved.
Remediation Steps
Core Principle: Never store a password in a form the application - or anyone with storage access - can read back; hash it with a slow, salted, purpose-built algorithm.
Trace the Data Path
- Source: Password entered at registration, login, or password-change
- Sink: Wherever it is persisted - a database column, a config file, a log line, a session/cache entry, an API response
- Missing control: No one-way hash applied before the value reaches that sink, or a hash applied with a fast general-purpose algorithm instead of a password-hashing one
Hash with a Password-Specific Algorithm (Primary Defense)
- Use Argon2id or scrypt, which are memory-hard as well as slow, so an attacker cannot buy speed with parallel hardware the way a GPU or an ASIC does against a CPU-bound hash. bcrypt is the fallback where neither is available: it is deliberately slow and CPU-hard, but its working set is a few kilobytes whatever the cost factor, so it is not memory-hard. OWASP scopes it to legacy systems, and CWE-916 covers the parameter choice
- Never use encryption, even strong modern encryption, for a password that only needs to be verified, never recovered. Encryption is reversible by design (see CWE-257 above); a one-way hash is not
- Never use a general-purpose fast hash such as MD5, SHA-1 or SHA-256/512, salted or not. These are designed to be fast, which is exactly the wrong property for password storage
- Give each password a unique, randomly generated salt so identical passwords do not produce identical hashes. Whether you have to do that yourself depends on the API rather than the algorithm. A password-hashing API generates the salt and stores it inside the hash string it returns -
argon2-cffi'sPasswordHasher, or bcrypt'sgensalt()/hashpw()pair. A raw KDF binding does not: Python'shashlib.scrypt()raisesTypeError: salt is requiredif you omit one, and storing it alongside the digest is then your job. Check which of the two you are holding before assuming it is handled
Keep Passwords Out of Every Non-Storage Path
- Never log a password, even at debug level or during troubleshooting
- Never accept a password as a URL or query parameter, where it ends up in server logs, browser history and proxy logs. Send it in the request body over HTTPS instead
- Never return a password, or its hash, in an API response
- Password-reset flows send a time-limited, single-use token, never the password itself
Migrate Existing Plaintext Storage
- Hash all existing plaintext passwords in place during a maintenance migration
- Force a password reset for affected accounts if the plaintext exposure window is a concern, for example because the plaintext already reached logs or backups before the fix
- Remove the plaintext column/field only after confirming the hashed column is populated and verified
Test the Fix
- Create an account and confirm the stored value is a hash (correct algorithm prefix, no resemblance to the original password), not the plaintext
- Confirm login still succeeds with the correct password and fails with an incorrect one
- Search recent logs for the literal test password to confirm it was never written
- Re-scan with the security scanner to confirm the plaintext-storage finding is resolved
Common Vulnerable Patterns
// VULNERABLE - password stored exactly as submitted
createUser(username, password):
db.insert(username, password) // no transformation at all
// Attack: read the users table (breach, backup, insider access)
// Result: every password recovered directly, no cracking required
Why this is vulnerable: what is stored is the credential itself, so the breach and the compromise are the same event. There is no work factor to raise, no cracking step to slow down and no window in which to rotate: whoever reads the row can authenticate as that user immediately. Backups, replicas, a database export attached to a support ticket and a developer's local copy all inherit that property, and each of those is normally protected as data rather than as a secret.
The damage also does not stop at this application. People reuse passwords, so a plaintext table is a working credential list for the same users' email, banking and employer accounts - which makes the value of the breach far larger than the value of the system it came from, and makes disclosure obligations correspondingly harder. This is the one weakness in the family where there is nothing to weigh: hashing with a purpose-built password function costs a login-path change and there is no situation in which the plaintext is needed, because verification only ever requires comparing a fresh hash against a stored one.
Secure Patterns
// SECURE - one-way, salted, slow hash applied before storage
createUser(username, password):
storedHash = passwordHash(password) // Argon2id/bcrypt/scrypt, unique salt
db.insert(username, storedHash)
verifyLogin(username, password):
storedHash = db.lookup(username)
return passwordHashVerify(storedHash, password)
Why this works: The stored value cannot be turned back into the password - verification only confirms a match, it never recovers the original. A stolen hash still requires the attacker to brute-force it at the algorithm's deliberately high cost per guess, rather than reading the password directly.
Common Pitfalls
- Encrypting instead of hashing: Passwords are run through AES or another reversible cipher "for protection" - anyone who also obtains the key (an attacker with application-server access, an insider, a misconfigured key store) recovers every password at once, unlike a hash which is designed never to be reversed. This is CWE-257's territory, not a fix for CWE-256.
- Hashing with a fast, general-purpose algorithm: MD5 or SHA-256 is applied before storage, which stops casual plaintext reading but not a real attack - these run billions of guesses per second on commodity hardware, so a leaked hash is cracked almost as fast as plaintext for common passwords. This addresses CWE-256 but lands in CWE-916 territory instead of solving the problem.
- Fixing storage but leaving the plaintext in transit logs: The database column is switched to a hash, but the password is still logged in an access log, error log, or APM trace on the way in - the plaintext is still recoverable, just from a different location.
- Migrating the schema without migrating the data: A
password_hashcolumn is added and used for new accounts, but existing rows in the oldpasswordcolumn are left as-is indefinitely - existing accounts remain exploitable exactly as before.