Skip to content

VNX-205: Observable Behavioral Discrepancy

Reports Python and JavaScript/TypeScript login code that returns before hashing the password when the account does not exist (if user is None: return, if (!user) return, except User.DoesNotExist: return, if not user or not check_password(...), if user and checkpw(...)), so response time reveals which accounts exist.

Updated 02 Nov 2026Checked against cli@de9b52a
Field Value
Rule ID VNX-205
Severity Low
SARIF level note
Kind sast
Languages Python, JavaScript / TypeScript
CWE CWE-205
CAPEC CAPEC-575
ATT&CK T1589
CWSS vector CWSS:1.0/TI:L/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:L/DI:H/EX:L/EC:N/P:L
Tags behavior-discrepancy timing account-enumeration authentication cwe-205
Source internal/sast/rules/vnx-205.rego

Overview

VNX-205 maps to CWE-205: Observable Behavioral Discrepancy. A password hash such as bcrypt or argon2 is deliberately slow. A login that only runs it when the account exists answers an unknown username in a millisecond and a known one with a wrong password in a hundred, even when both return the same message. Timing the responses enumerates accounts.

What Gets Flagged

A check that the looked-up account is missing whose branch returns, raises, aborts or throws without hashing, followed within 12 lines by a password verification of that account (bcrypt.checkpw, check_password, check_password_hash, verify_password, pwd_context.verify, argon2/hasher .verify, bcrypt.compare); and the short-circuit form where the hash is skipped for a missing account.

user = User.query.filter_by(email=request.form["email"]).first()
if user is None:                                                    # flagged
    abort(401)
if not bcrypt.checkpw(request.form["password"].encode(), user.password_hash):
    abort(401)
if (!account || !(await bcrypt.compare(req.body.password, account.hash))) {  // flagged
  return res.sendStatus(401);
}

Not flagged: a branch that verifies against a dummy hash or calls set_password/hashpw first (as Django’s ModelBackend does), a verification that runs before the account check, a missing-record check with no password verification after it, comments and strings.

Remediation

Run the hash on every path:

DUMMY_HASH = bcrypt.hashpw(b"unused", bcrypt.gensalt())

hashed = user.password_hash if user else DUMMY_HASH
ok = bcrypt.checkpw(password.encode(), hashed)
if user is None or not ok:
    abort(401)

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-205
// nosec vnx-205

Or record an ignore rule: vulnetix ignore add --rule vnx-205 --reason "...". See Suppressing findings.

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