VNX-759: Password hashed with a one-way hash and no salt
A password is passed straight into a fast general-purpose digest with no salt, so equal passwords get equal hashes and a precomputed table recovers them. Reported, when the hashed argument names a password (password, passwd, passphrase, pwd, pw, $pass, $pass_new, getPassword(), a subscript or .get() keyed by a password field such as form["new_password"] or request.json.get("password")) or a variable assigned from one a few lines above: Python hashlib.md5/sha1/sha224/sha256/sha384/sha512/sha3_*/blake2*/new(...) (also from-imported) and <hashlib object>.update(password); Java MessageDigest <md>.digest(password...)/<md>.update(password...) and MessageDigest.getInstance(...).digest(password...), commons-codec DigestUtils.md5Hex/sha1Hex/sha256Hex/... and Guava Hashing.md5()/sha1()/sha256()/sha512().hashString/hashBytes; PHP md5()/sha1()/crc32()/hash('<alg>', ...); Node crypto.createHash(...).update(password) on one line, as a chained call split over lines, or through a variable (const h = crypto.createHash(...); h.update(password)). A call argument split onto the next line is followed. Not reported: an argument or a neighbouring update() that mixes in a salt or pepper, password KDFs (pbkdf2_hmac, scrypt, bcrypt, argon2, PBEKeySpec, password_hash, scryptSync), HMAC, digests of tokens, nonces, password-reset values, file contents or other non-password data, string literals and comments. Weak iteration counts and broken algorithms are other rules (VNX-CRYPTO-007, VNX-327). A password passed straight into a fast digest call (md5($pass), hashlib.sha256(password.encode()), createHash('sha256').update(password), DigestUtils.sha256Hex(password)) is reported by VNX-CRYPTO-007, a password hashed with a fast hash, whose fix (a salted password KDF) is this rule's fix too; this rule keeps the shapes that rule does not read, such as a password reached through a variable, a subscript or a hasher fed on a later line.
| Field | Value |
|---|---|
| Rule ID | VNX-759 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Java, Python, PHP, JavaScript / TypeScript |
| CWE | CWE-759 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:H/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | password-hash unsalted cwe-759 |
| Source | internal/sast/rules/vnx-759.rego |
Overview
VNX-759 maps to CWE-759: Use of a One-Way Hash without a Salt. It reports a fast general-purpose digest whose input is a password (an argument named password, passwd, pwd, $pass, getPassword() and similar, or a variable assigned from one a few lines above) with no salt or pepper mixed into the same digest: Python hashlib.* and h.update(password), Java MessageDigest.digest/update, DigestUtils.*Hex and Guava Hashing.*().hashString, PHP md5(), sha1(), crc32() and hash('<alg>', ...), and Node crypto.createHash(...).update(password).
It does not report salted digests, password KDFs (pbkdf2_hmac, scrypt, bcrypt, Argon2, PBEKeySpec, password_hash), HMAC, or digests of tokens, nonces, reset values and file contents.
A password passed straight into a fast digest call (md5($pass), hashlib.sha256(password.encode()), createHash('sha256').update(password)) is reported by VNX-CRYPTO-007, whose fix is the same salted password KDF, so the line is reported once. This rule keeps what that rule does not read: a password reached through a variable assigned a few lines above, a request field, or a hasher fed on a later line.
Why This Matters
Without a salt, two users with the same password get the same hash, and a single precomputed (rainbow) table reverses every hash in a leaked table at once. Fast digests also let an attacker try billions of guesses per second.
What Gets Flagged
cred = request.form["new_password"]
user.password_digest = hashlib.sha256(cred.encode()).hexdigest() # flagged
MessageDigest md = MessageDigest.getInstance("SHA-256");
return md.digest(password.getBytes(StandardCharsets.UTF_8)); // flagged
const h = crypto.createHash('sha256');
h.update(password); // flagged
Reported by VNX-CRYPTO-007 instead: $pass = md5($pass);, hashlib.sha256(password.encode()), crypto.createHash('sha256').update(password).
Remediation
- Hash passwords with a password KDF that salts for you: bcrypt, scrypt, Argon2id or PBKDF2 with a high iteration count (
password_hash()in PHP,BCryptPasswordEncoderin Spring). - Migrate existing hashes on the next successful login.
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-759
// nosec vnx-759
Or record an ignore rule: vulnetix ignore add --rule vnx-759 --reason "...". See Suppressing findings.