VNX-327: Broken hash algorithm (MD5, SHA-1) protecting a password, token or signature
MD5 or SHA-1 is computed through a hash API on a security value: the call line, the three code lines above it or the next one (the next three for a Java/Kotlin MessageDigest, which is fed afterwards), never looking past a line that closes a block name a password, secret, token, session id, cookie, signature, salt, nonce, CSRF value, API key, credential or reset value. Both algorithms are broken for collision resistance and are fast to brute-force, so they must not hash passwords or derive tokens and signatures; use SHA-256 or stronger, HMAC-SHA256 for tokens and signatures, and argon2id/bcrypt/scrypt for passwords. Calls recognised: Python hashlib.md5()/sha1()/new('md5'|'sha1') and bare md5()/sha1() after from hashlib import, Node.js createHash('md5'|'sha1') and CryptoJS.MD5/SHA1, Java/Kotlin MessageDigest.getInstance("MD5"|"SHA-1"), commons-codec/Spring DigestUtils.md5Hex/sha1Hex/md5DigestAsHex and Guava Hashing.md5()/sha1(), PHP md5()/sha1()/md5_file()/sha1_file(), Ruby Digest::MD5/SHA1, OpenSSL::Digest::MD5/SHA1 and OpenSSL::Digest.new('MD5'|'SHA1'). Not reported: digests with no security name nearby (cache keys, ETags, checksums, content addresses), digests of a constant (static::class, __CLASS__, a string literal), Python calls marked usedforsecurity=False, HMAC-SHA1, names, fields, enums and comments that only mention the algorithms, and test files. A password passed straight into the digest (md5($pass), hashlib.md5(password.encode()), createHash('md5').update(password), DigestUtils.md5Hex(password), Digest::MD5.hexdigest(password)) is reported by VNX-CRYPTO-007, a password hashed with a fast hash, and not here; this rule keeps tokens, session ids, cookies, signatures, salts, nonces and keys, and a password named only around the call. Other MD5 and SHA-1 calls are reported by VNX-CRYPTO-001 and VNX-CRYPTO-002; Go is covered there.
| Field | Value |
|---|---|
| Rule ID | VNX-327 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Java, Kotlin, JavaScript / TypeScript, PHP, Python, Ruby |
| CWE | CWE-327 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | crypto weak-algorithm cwe-327 |
| Source | internal/sast/rules/vnx-327.rego |
Overview
VNX-327 maps to CWE-327: Use of a Broken or Risky Cryptographic Algorithm. It reports a call into MD5 or SHA-1 when the value being hashed or produced is a security value: the call line, the three code lines above it or the next line (the next three for a Java/Kotlin MessageDigest, which is fed afterwards) name a password, secret, token, session id, cookie, signature, salt, nonce, CSRF value, API key, credential or reset value.
A password passed straight into the digest (md5($pass), hashlib.md5(password.encode()), createHash('md5').update(password), DigestUtils.md5Hex(password), Digest::MD5.hexdigest(password)) is reported by VNX-CRYPTO-007, a password hashed with a fast hash, and not here, so the line is reported once with the password-hashing fix.
Why This Matters
Both algorithms are broken for collision resistance and are fast enough to brute-force billions of guesses a second, so a password hashed with them is recovered from a leaked database, and a token or signature derived from them can be forged or predicted.
What Gets Flagged
# FLAGGED
return hashlib.md5(f"{email}:{time.time()}".encode()).hexdigest() # in make_reset_token
// FLAGGED
const signature = CryptoJS.SHA1(secret + body).toString(CryptoJS.enc.Hex);
// FLAGGED: the digest is fed a password below
MessageDigest md = MessageDigest.getInstance("MD5");
// FLAGGED
$_SESSION['session_token'] = md5(uniqid());
# FLAGGED
OpenSSL::Digest::SHA1.hexdigest("#{ACCESS_TOKEN_SALT}:#{id}")
What Is Not Flagged
- Digests with no security name nearby: cache keys, ETags, checksums, content addresses.
- Digests of a constant (
sha1(static::class),__CLASS__, a string literal). - Python calls marked
usedforsecurity=False, and HMAC-SHA1. - Fields, enums and comments that only mention the algorithms, and test files.
- A password hashed directly (
$pass = md5($pass),Digest::MD5.hexdigest(password)): reported by VNX-CRYPTO-007.
Other MD5 and SHA-1 calls, including Go, are reported by VNX-CRYPTO-001 and VNX-CRYPTO-002.
Remediation
- Hash passwords with argon2id, bcrypt or scrypt.
- Derive tokens from a CSPRNG (
secrets.token_urlsafe(32)), and sign with HMAC-SHA256. - Use SHA-256 or stronger wherever a digest protects integrity.
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-327
// nosec vnx-327
Or record an ignore rule: vulnetix ignore add --rule vnx-327 --reason "...". See Suppressing findings.