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.
| 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.