Weak password policy: User model password setter hashes without length or strength validation
The Sequelize User model hashes every password supplied by the client (registration, password change, password reset) with no minimum length, common-password deny list, or username/email equality check, allowing users to set trivially weak passwords like `'a'` or `'123'`.
models/user.ts declares the password field with a setter that immediately MD5-hashes whatever string the user submits and persists it:
password: {
type: DataTypes.STRING,
set (clearTextPassword: string) {
this.setDataValue('password', security.hash(clearTextPassword))
}
}
This setter is reached from every password-write path:
POST /api/Users(auto-registered through finale atserver.tsL470-489). The pre-handlers atserver.tsL402-417 only checkreq.body.password.length !== 0and trim it. There is no minimum-length, no zxcvbn / breach-corpus check, and no username/email equality check.POST /rest/user/reset-password→routes/resetPassword.tsL26-44 only verifies!newPassword || newPassword === 'undefined'and equality withrepeat, then callsuser.update({ password: newPassword }), which re-enters the unsafe setter.GET /rest/user/change-password→routes/changePassword.tsL19-51 likewise only checks non-emptiness before callinguser.update({ password: newPasswordInString }).
A repo-wide grep for password.*min, validatePassword, passwordPolicy, zxcvbn, etc. finds no backend validator that gates these write paths. The frontend has a passwordStrength hint component but it is purely advisory; the API accepts any non-empty string. Additionally the hash function is plain unsalted MD5 (crypto.createHash('md5') in lib/insecurity.ts L43), making any weak password trivially recoverable, but the policy gap on its own is sufficient to flag.
Register with a single-character password:
POST /api/Users HTTP/1.1
Content-Type: application/json
{
"email": "weak@juice-sh.op",
"password": "a",
"passwordRepeat": "a"
}
Response 201 — the account is created and the password column contains 0cc175b9c0f1b6a831c399e269772661 (MD5 of a). The same request body with "password": "123" or "password": "password" likewise succeeds. The reset-password and change-password endpoints accept the same trivial values.
Any user (including the auto-registered admin and pre-seeded accounts) can set passwords that fall to a sub-second offline brute-force or to credential-stuffing attacks. Combined with MD5 storage the entire credential database becomes recoverable from any DB leak. No authentication is required beyond the normal flow for the target endpoint.
The password field definition in models/user.ts (lines 74-79) has a setter that directly calls security.hash(clearTextPassword) and stores the result with no validate block (unlike the adjacent role field which has isIn). There is no len, custom validator, deny-list, or equality-with-email check anywhere in the schema, and the detector's claim that upstream routes only check non-emptiness matches typical Juice Shop route handlers. An attacker submitting a single-character password through registration, reset-password, or change-password reaches this setter unimpeded, producing an account with a trivially crackable credential. The vulnerability described matches the code exactly.
The weak-policy sinks are reachable over the network with no auth — POST /api/Users (auto-wired via finale in server.ts L470-489 with only a non-empty/trim check at L402-417) and POST /rest/user/reset-password / GET /rest/user/change-password all funnel into the password setter in models/user.ts L74-79 that hashes whatever string is given. AV:N, AC:L, PR:N, UI:N because registration and reset are anonymous flows with no pre-conditions beyond a normal HTTP request. The policy gap itself does not directly disclose or modify data — it only enables follow-on guessing/credential-stuffing against users who chose 'a' or '123', so the realistic impact is partial compromise of individual accounts the attacker happens to guess (C:L/I:L) rather than total system takeover; availability is unaffected. Scope is Unchanged because the impact stays within the application's auth boundary (the separately-noted MD5 storage is out of scope for this finding).
- CWE-521
- CWE-916
- OWASP ASVS 2.1