Skip to content

VNX-385: Covert Timing Channel

A password, or a password hash, is checked with an ordinary equality comparison, which returns as soon as the first byte differs, so the time a login takes tells an attacker how much of a guess is right (an observable timing channel; storing or comparing the plaintext password is a further weakness). Reported in Python, JavaScript/TypeScript and Java: ==, !=, === or !== where one operand is named as a password (password, passwd, passphrase, pwd, including user.password, row['password'], getPassword(), passwordHash, hashed_password, adminPassword) and the other operand is a real value; Java a.equals(b)/equalsIgnoreCase, Objects.equals and Arrays.equals with such an operand. Comparisons that continue onto the next line are joined, including a line break before the operator or .equals(. Not reported: presence checks against an empty string, None, null, undefined, a boolean or a number; a string literal or ALL_CAPS constant operand (hard-coded credentials are reported by other rules); confirmation, policy, length, field, type and similar names (password_confirmation, passwordPolicy, password.length); a new-versus-old password reuse check; `pwd` naming the working directory (assigned from a cwd call, user.dir or PWD in the same file); lines that already use a constant-time or hashing comparison (hmac.compare_digest, secrets.compare_digest, crypto.timingSafeEqual, MessageDigest.isEqual, bcrypt/check_password/PasswordEncoder.matches); the text in strings and comments; and test code. MAC, signature and token comparisons are reported by VNX-CRYPTO-008. Two comparisons on the same timing channel are reported by a more precise rule, so the line is reported once: both operands naming the password (user.password == request.form["password"]) by VNX-309, a login decided by a plain password comparison, and a password compared with a value named as a secret, token, digest, signature or MAC next to the hash call or request read that produced it by VNX-CRYPTO-008.

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

Overview

VNX-385 maps to CWE-385: Covert Timing Channel. It reports ==, !=, === or !== where one operand is named as a password (password, passwd, passphrase, pwd, including user.password, row['password'], getPassword(), passwordHash, hashed_password) and the other is a real value, plus Java a.equals(b)/equalsIgnoreCase, Objects.equals and Arrays.equals with such an operand. Comparisons continued on the next line are joined.

Two shapes are reported by a more precise rule, so the same line is not reported twice: both operands naming the password (user.password == request.form["password"]) by VNX-309, a login decided by a plain password comparison, and a password compared with a value named as a secret, token, digest, signature or MAC (answerPwd.equals(md5Secret)) by VNX-CRYPTO-008.

Not reported: presence checks against an empty string, None, null, undefined, a boolean or a number; a string literal or ALL_CAPS constant operand (hard-coded credentials have their own rules); confirmation, policy, length and field names (password_confirmation, passwordPolicy, password.length); new-versus-old reuse checks; lines that use a constant-time or hashing check (hmac.compare_digest, crypto.timingSafeEqual, MessageDigest.isEqual, bcrypt, check_password, PasswordEncoder.matches); strings, comments and test code. MAC, signature and token comparisons are reported by VNX-CRYPTO-008.

Why This Matters

An ordinary string or array comparison stops at the first differing byte, so response time reveals how many leading bytes of a guess are right. A direct comparison usually also means the password, or an unsalted hash of it, is stored in a form that can be compared byte for byte.

What Gets Flagged

if user.password != hash_value:                              # FLAGGED
if (user.password !== security.hash(password)) {             // FLAGGED
return u != null && u.getPassword().equals(password);       // FLAGGED
return Arrays.equals(u.getPasswordHash(), digest(candidate)); // FLAGGED

Reported by VNX-309 instead: user.password == request.form["password"], user.password === req.body.password. Not flagged: bcrypt.checkpw(pw, user.password_hash), encoder.matches(raw, user.getPassword()), if password == "":, password == password_confirmation.

Remediation

  1. Store passwords with a slow, salted hash (bcrypt, scrypt, Argon2, PBKDF2) and verify with the library’s own check.
  2. Where two secrets must be compared directly, use a constant-time function (hmac.compare_digest, crypto.timingSafeEqual, MessageDigest.isEqual).

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

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

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