agentggagentgg
Back to all findings
MEDIUMconfirmedweak-password-policyweak-password-policy5e34f2f54d08

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

Filemodels/user.ts
Lines7479
Confidence
90%
File statusvalidated
Details

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 at server.ts L470-489). The pre-handlers at server.ts L402-417 only check req.body.password.length !== 0 and trim it. There is no minimum-length, no zxcvbn / breach-corpus check, and no username/email equality check.
  • POST /rest/user/reset-passwordroutes/resetPassword.ts L26-44 only verifies !newPassword || newPassword === 'undefined' and equality with repeat, then calls user.update({ password: newPassword }), which re-enters the unsafe setter.
  • GET /rest/user/change-passwordroutes/changePassword.ts L19-51 likewise only checks non-emptiness before calling user.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.

Proof of concept

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.

Impact

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.

Validation
confirmed

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.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
Base score: 6.5 · MEDIUM

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

References