Skip to content

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.

Updated 02 Nov 2026Checked against cli@de9b52a
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

  1. Store passwords with a slow, salted hash (Argon2id, bcrypt, scrypt) and verify with the library’s constant-time check.
  2. 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.

Edit this pageSources: cli/internal/sast/rules/vnx-309.rego, cli/internal/sast/docs/vnx-309.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.