Password reset accepts arbitrarily-weak passwords (no length/strength enforcement)
The reset flow writes `req.body.new` to the user record via `user.update({ password: newPassword })` after only checking that the value is non-empty — no minimum length, no complexity rule, no breach/common-password check, and the model's password setter just MD5-hashes whatever it receives.
In routes/resetPassword.ts (lines 16–45) the only validation gates on the new password are:
if (!newPassword || newPassword === 'undefined') {
res.status(401).send(res.__('Password cannot be empty.'))
return
}
if (newPassword !== repeatPassword) {
res.status(401).send(res.__('New and repeated password do not match.'))
return
}
// ...
const updatedUser = await user.update({ password: newPassword })
There is no password.length >= 8, no zxcvbn, no common-password list, and no equality check against email/username.
Tracing the value into the model (models/user.ts, lines 74–79):
password: {
type: DataTypes.STRING,
set (clearTextPassword: string) {
this.setDataValue('password', security.hash(clearTextPassword))
}
}
The setter calls security.hash, which in lib/insecurity.ts is plain MD5:
export const hash = (data: string) => crypto.createHash('md5').update(data).digest('hex')
So a user (or someone who has answered the security question) can set password: "a", password: "123", or any top-10 common password, and it will be MD5-hashed and stored. There is no chain of middleware enforcing a policy either — server.ts registers POST /rest/user/reset-password directly to this handler.
- Answer the security question challenge (or be the victim of a CSRF/XSS that drives this endpoint).
POST /rest/user/reset-password{ "email": "victim@…", "answer": "…", "new": "a", "repeat": "a" }.- Server responds 200 and persists MD5("a") as the user's password. The account now has a trivially-guessable credential.
Account takeover / credential stuffing exposure: any user (including an attacker who guessed the security answer) can set a single-character or top-10 common password, which combined with the MD5-only storage means an attacker who later obtains the DB can crack every reset password instantly.
The handler in routes/resetPassword.ts only validates !newPassword || newPassword === 'undefined' and newPassword !== repeatPassword before calling user.update({ password: newPassword }) on line 41 — there is no minimum-length, complexity, or common-password check. Per the scope rule ("Treat every finding as if this were a real production application"), this absence of a password policy on a reset endpoint is a legitimate weak-password-policy bug, and the PoC (set new/repeat to "a") is consistent with the code path shown.
The /rest/user/reset-password endpoint is reachable over the network (AV:N) and requires no prior authentication to the application itself — only knowledge of the security answer, which is part of the exploit chain rather than a separate privilege (PR:N). Because the only validation in routes/resetPassword.ts lines 37–44 is non-empty and matching-repeat, an account ends up with a trivially crackable password only if a legitimate user actually chooses a weak value or an attacker first guesses the security answer (UI:R, AC:H — chained pre-requisite plus reliance on victim behavior or DB exfiltration to monetize the MD5-hashed credential). Successful exploitation yields control of a single user account, so confidentiality and integrity impact are Low and scope stays within the application (S:U, C:L, I:L); there is no availability effect.
- CWE-521
- CWE-916
- OWASP A07:2021 Identification and Authentication Failures