VNX-309: Use of Password System for Primary Authentication
Login decided by comparing a submitted password directly with a stored or configured one (==, ===, !=, .equals()) instead of verifying a slow salted hash.
| Field | Value |
|---|---|
| Rule ID | VNX-309 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Java, JavaScript / TypeScript, PHP, Python |
| CWE | CWE-309 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | password-as-primary cwe-309 |
| Source | internal/sast/rules/vnx-309.rego |
Overview
VNX-309 maps to CWE-309: Use of Password System for Primary Authentication. Whether a deployment adds a second factor elsewhere is not visible in code, so the rule reports the concrete code-level form a line-oriented check can see: authentication decided by an equality test whose two operands both name a password, in Python, JavaScript/TypeScript, Java or PHP. Examples are user.password == request.form["password"], password === user.password, user.getPassword().equals(password), Objects.equals(password, account.getPassword()) and $row['password'] == $_POST['password']. Such code keeps the password in a recoverable form, makes it the sole secret, and compares it in non-constant time.
Not reported: password-hash verification (check_password_hash, bcrypt.compare, passwordEncoder.matches, password_verify), operands that hash or measure the password (hash(), md5, digest, length), confirmation checks (password == confirm_password, password1 != password2, new versus current password, matching password), comparisons with null, empty strings or other literals, enum and format comparisons, test files and comments.
Why This Matters
A plain comparison means the password is stored in cleartext (or reversibly), so a database leak hands over every credential directly, and those credentials are often reused on other sites. With nothing but the password protecting the account, guessing or phishing it is enough.
What Gets Flagged
# FLAGGED
if u and u.password == request.form["password"]:
session["uid"] = u.id
// FLAGGED
if (!user || user.password !== req.body.password) { return res.status(401).end(); }
// FLAGGED
if (account != null && account.getPassword().equals(password)) { return true; }
Not flagged: check_password_hash(user.password_hash, form["password"]), await bcrypt.compare(password, user.passwordHash), data.get("password1") != data.get("password2"), password === ''.
Remediation
- Store passwords with a slow, salted hash (Argon2id, bcrypt, scrypt) and verify with the library’s constant-time check.
- Add a second factor (TOTP, WebAuthn) for login, or move to passwordless authentication.
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-309
// nosec vnx-309
Or record an ignore rule: vulnetix ignore add --rule vnx-309 --reason "...". See Suppressing findings.