Safeguard
Application Security•

CWE-759 One-Way Hash Without a Salt: Password Storage Done Right

Unsalted password hashes fall to precomputed tables. Learn why salt alone is not enough, and what MITRE recommends instead for storing passwords.

Safeguard Security Team
4 min read

CWE-759 is the password storage mistake most developers have heard of. The product runs a one-way cryptographic hash over an input that should not be reversible, such as a password, but does not add a salt to the input.

Any application with its own login, and any device with an admin password, is in scope. The record's examples are routers.

How it happens

A hash is deterministic. The same password always produces the same output. Without a salt, two users with the same password have identical stored hashes, and an attacker can compute hashes of common passwords once and compare them against every account. Lists of precomputed hashes are known as rainbow tables.

The code is usually one line:

# Vulnerable: plain fast hash, no salt
import hashlib
stored = hashlib.sha256(password.encode()).hexdigest()

MITRE lists CVE-2008-1526 and CVE-2006-1058, both routers that did not use a salt with a hash, making passwords easier to crack.

What an attacker gains

The consequence in the record is bypassing a protection mechanism and gaining privileges or assuming an identity. It starts with access to the hashes, through a database leak, a backup, or a config file. Once an attacker has them, the missing salt makes brute force with rainbow tables easier. Cracked passwords become logins, and users reuse passwords, so the damage can reach other systems.

A point many articles skip

MITRE adds a caution that changes how you should fix this. A good salt alone does not sufficiently raise the effort for an attacker who targets one password or who has large computing resources, such as cloud services or inexpensive specialized hardware. Offline cracking stays effective if the hash is cheap to compute. Many cryptographic hashes are built to be efficient, so even a strong one can be attacked at massive scale. A salt only slightly increases the computing requirements compared with other strategies such as adaptive hash functions.

In short, salting defeats precomputation. It does not defeat guessing.

How to find it

MITRE cites several techniques from a government software assurance review. For source code, a source code weakness analyzer and focused manual review of the spots where passwords are handled are rated highly cost effective. For compiled code, bytecode and binary weakness analysis, or a disassembler plus manual review, give partial coverage.

A simple manual check: find every place a password is hashed and read the call. A bare md5, sha1 or sha256 over the password is the finding. Also check how existing credentials are stored in the database, since old rows may predate a fix.

How to fix it

MITRE's mitigations, in order of strength:

  1. Use an adaptive hash function. These let you tune the work needed, through iterations ("stretching") or memory, and some salt automatically. They raise the cost of brute force far above fast functions such as MD5, and can make rainbow table attacks infeasible. MITRE names bcrypt, scrypt and PBKDF2, and notes active debate over which is best while saying all are stronger than salting a cheap hash. Effectiveness: high.
  2. Mind performance. These functions use real CPU and memory, so choose parameters with denial of service in mind. The advantage is that you can raise the workload later without replacing the algorithm.
  3. If you truly cannot do that, salt properly. Generate a new random salt per password with a strong random number generator, add it before hashing, store it with the hash, and never use the same salt for every password. MITRE rates this limited.
# Fix: scrypt with a per-password random salt
import hashlib, os, hmac
def hash_password(password):
    salt = os.urandom(16)
    h = hashlib.scrypt(password.encode(), salt=salt, n=2**14, r=8, p=1)
    return salt + h

def verify(password, stored):
    salt, h = stored[:16], stored[16:]
    candidate = hashlib.scrypt(password.encode(), salt=salt, n=2**14, r=8, p=1)
    return hmac.compare_digest(h, candidate)

Store the parameters with the hash as well, so you can raise them later and rehash users as they log in.

Click through it at your own pace, or open it full screen.

Quick check

What does a salt prevent?

Precomputed lookups such as rainbow tables, and identical hashes for identical passwords. It does not stop a determined attacker from guessing against a fast hash.

What does MITRE recommend over salting a fast hash?

An adaptive hash function such as bcrypt, scrypt or PBKDF2, which makes each guess expensive and can be tuned over time.

Can one salt be shared by every user?

No. A shared salt lets an attacker attack all accounts at once. See CWE-760.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.