CWE-256 is blunt: the product stores a password in plaintext within resources such as memory or files. MITRE lists it as not language specific, and flags ICS/OT as a relevant platform.
It is old and well understood, and it keeps appearing. The reason is rarely ignorance. It is usually a shortcut that nobody went back to fix.
How it happens
A few paths lead here.
- A prototype stores credentials in a config file or a database column "for now", and the prototype becomes the product.
- A system needs to log in to something else, so it keeps the password in a form it can read back.
- A developer writes passwords to a log line while debugging.
- A settings export, backup or API response includes the field by accident.
The password may be for users logging in to the product, or for the product logging in to another system. Those are two different problems with different fixes, and it helps to be clear which one you have.
Vulnerable pattern for user accounts, in Python:
# Vulnerable: the password is stored exactly as the user typed it
def register(db, username, password):
db.execute("INSERT INTO users (name, password) VALUES (?, ?)",
(username, password))
def login(db, username, password):
row = db.execute("SELECT password FROM users WHERE name = ?",
(username,)).fetchone()
return row is not None and row[0] == password
Fixed, with a salted slow hash from the standard library:
import hashlib, hmac, os
def hash_password(password: str, salt: bytes = None):
salt = salt or os.urandom(16)
digest = hashlib.scrypt(password.encode(), salt=salt,
n=2**14, r=8, p=1)
return salt, digest
def register(db, username, password):
salt, digest = hash_password(password)
db.execute("INSERT INTO users (name, salt, hash) VALUES (?, ?, ?)",
(username, salt, digest))
def login(db, username, password):
row = db.execute("SELECT salt, hash FROM users WHERE name = ?",
(username,)).fetchone()
if row is None:
return False
_, candidate = hash_password(password, row[0])
return hmac.compare_digest(candidate, row[1])
The stored value can no longer be turned back into the password. Verification hashes the submitted value and compares. A dedicated password hashing library for your framework is usually a better choice than hand rolled code, and the point of the sketch is the shape of the solution, not the exact parameters.
What an attacker gains
MITRE lists access control: gaining privileges or assuming an identity. Storing a plaintext password in a configuration file allows anyone who can read the file to access the password protected resource. In some contexts, even a plaintext password in memory is considered a risk if the password is not cleared promptly.
The practical effect is that every other weakness that exposes a file or a memory region becomes more serious. A path traversal bug that reads a config file, a misconfigured backup bucket, a stolen laptop or a crash dump all become a source of working credentials.
MITRE's observed example is CVE-2022-30275. A remote terminal unit (RTU) used a driver that relied on a password stored in plaintext. That is the ICS/OT context the record calls out: devices that run for years, rarely get touched, and sit where an attacker would like to be.
How to find it
MITRE lists automated static analysis, which can model data flow and look for patterns where a password reaches a file, a log or a database without being transformed.
Practical checks you can run this week:
- Search the code, config templates and container images for keys named password, passwd, secret and token, and look at the values.
- Look at the schema. A password column that holds readable values, or a column called something like
plain, tells the story quickly. - Read your logs. Grep for a test account's password and see where it shows up.
- Inspect exports, debug endpoints and backups for credential fields.
- Run a secrets scan on every commit so a committed password is caught before it spreads.
How to fix it
MITRE gives two mitigations, both in Architecture and Design.
- Avoid storing passwords in easily accessible locations.
- Consider storing cryptographic hashes of passwords as an alternative to storing in plaintext. MITRE adds a warning that obscuring a password with an encoding function such as base 64 does not protect it, because the encoding is easy to detect and decode. It rates the effectiveness of that approach as none.
Turn that into a plan.
- For passwords your product verifies, store a salted one way hash made with a slow password hashing function. Never store the password.
- For passwords your product must present to another system, keep them out of source files and plain config. Load them at runtime from a protected store, and limit who and what can read that store.
- Keep passwords out of logs, error messages, exports and API responses.
- Clear secrets from memory when you are done with them.
- Rotate any credential that was ever stored in plaintext. Fixing the code does not undo past exposure.
Try it yourself
Run a secrets scan on a sample repository and see how plaintext credentials are flagged.
Click through it at your own pace, or open it full screen.
Quick check
Does encoding a password count as protection?
No. MITRE notes that an encoding such as base 64 is easy to detect and decode, and rates its effectiveness as none.
What is the difference between a stored user password and a stored outbound password?
A user password should be stored as a salted one way hash and verified by comparison. A password your product sends to another system has to be readable by the product, so it belongs in a protected secret store, not in code or plain config.
Why does this weakness make other bugs worse?
Any bug that exposes a file or memory then also exposes working passwords.