VNX-640: Weak Password Recovery Mechanism for Forgotten Password
Detects weak forgotten-password flows: reset tokens, OTPs and temporary passwords from a non-cryptographic source (random, Math.random, java.util.Random, time), reset links built from the Host header, and resets decided by a security question. JavaScript/TypeScript, Python and Java.
| Field | Value |
|---|---|
| Rule ID | VNX-640 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | JavaScript / TypeScript, Python, Java |
| CWE | CWE-640 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | weak-password-recovery password-reset host-header-poisoning cwe-640 |
| Source | internal/sast/rules/vnx-640.rego |
Overview
VNX-640 maps to CWE-640: Weak Password Recovery Mechanism for Forgotten Password. A recovery flow is a second way to log in, and it is usually weaker than the first. Three concrete weaknesses are reported:
- A predictable recovery secret. A reset/recovery/forgot token, code, key, hash, link or pin, an OTP or one-time code, a verification code, or a temporary or new password generated by a non-cryptographic source: Python
random.*(orrandint/choiceimported fromrandom),time.time(),uuid1(), a hash of the email, username or time; JavaScriptMath.random(),Date.now(),new Date().getTime(); Javanew Random(), ajava.util.Randomfield’snext*(),ThreadLocalRandom,RandomStringUtils.random*,System.currentTimeMillis(). These values can be predicted or brute-forced. - A reset link built from the Host header (password-reset poisoning): Flask
request.host/host_url/url_root, DjangoMETA['HTTP_HOST'], Expressreq.headers.host/req.get('host')/req.hostname, theX-Forwarded-Hostheader in Python and Express, ServletgetHeader("Host")/getServerName(), used in a link or passed with the reset token near reset/forgot/recover code. An attacker who sets the header to their own domain receives the victim’s token when the victim clicks. - A reset decided by a security question: in a file that names a security or secret question/answer, a comparison of the stored answer with an answer from the request. Answers to “first pet” questions are guessable and often public.
A timestamp stored beside a token (resetPasswordExpires = Date.now() + 3600000, reset_token_created = time.time()) is not a secret and is not reported.
What Gets Flagged
reset_token = str(random.randint(100000, 999999)) # FLAGGED
link = f"http://{request.host}/reset/{token}" # FLAGGED
if user.security_answer.lower() == request.form["answer"].lower(): # FLAGGED
const resetToken = Math.random().toString(36).slice(2); // FLAGGED
String resetToken = RandomStringUtils.randomAlphanumeric(32); // FLAGGED
String host = request.getHeader("Host"); // FLAGGED (used in the reset mail)
What Is Not Flagged
- Tokens from
secrets,random.SystemRandom,crypto.randomBytes/randomInt,SecureRandomorUUID.randomUUID(). - Random values for non-secret purposes (jitter, retry delays) and timestamps such as
createdAt. - Host reads unrelated to a reset, reset links built from configuration, and Django
get_host()(validated againstALLOWED_HOSTS). - Test code, comments and string literals.
Remediation
Generate a single-use, expiring token from a CSPRNG, build the link from a configured base URL, and send it to the registered address instead of asking a security question:
token = secrets.token_urlsafe(32)
link = f"{settings.PUBLIC_BASE_URL}/reset/{token}"
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-640
// nosec vnx-640
Or record an ignore rule: vulnetix ignore add --rule vnx-640 --reason "...". See Suppressing findings.