#11323·better-auth

`maxPasswordLength` is not enforced before hashing on sign-in, verify-password, change-password (currentPassword), delete-user, two-factor and admin create-user

Author: XXMOHAMED012Created Sep 17, 2026Updated Sep 17, 2026

Is this suited for github?

  • Yes, this is suited for github

Reproduction

Default config, default scrypt hasher, [email protected] and main (41b7dc1).

typescript
const auth = betterAuth({ emailAndPassword: { enabled: true }, /* any adapter */ });

// 1. sign-up enforces the bound before hashing
await auth.api.signUpEmail({ body: { name: "a", email: "[email protected]", password: "x".repeat(129) } });
// -> APIError 400 PASSWORD_TOO_LONG, no hash computed

// 2. sign-in does not
await auth.api.signInEmail({ body: { email: "[email protected]", password: "x".repeat(1024 * 1024) } });
// -> APIError 401 INVALID_EMAIL_OR_PASSWORD, after a full scrypt over the 1 MiB input

Same shape on every endpoint in the table below: the input goes straight to ctx.context.password.hash / .verify with no length check.

Current vs. Expected behavior

Current. maxPasswordLength (default 128, context/create-context.ts) is checked on the seven endpoints that set a password, and skipped on the eleven that verify one (plus /admin/create-user, which hashes with neither a min nor a max):

Endpoint Call site on main Unchecked field
POST /sign-in/email api/routes/sign-in.ts:539,550,557 password
POST /sign-in/username plugins/username/index.ts:472,496 password
POST /sign-in/phone-number plugins/phone-number/routes.ts:162 password
POST /change-password api/routes/update-user.ts:277 currentPassword (newPassword is checked at 262)
POST /delete-user api/routes/update-user.ts:483 password
POST /verify-password api/routes/password.ts:379utils/password.ts:17 password
POST /two-factor/enable plugins/two-factor/index.ts:185utils/password.ts:17 password
POST /two-factor/disable plugins/two-factor/index.ts:365utils/password.ts:17 password
POST /two-factor/generate-backup-codes plugins/two-factor/backup-codes/index.ts:494utils/password.ts:32,36 password
POST /two-factor/get-totp-uri plugins/two-factor/totp/index.ts:216utils/password.ts:32,36 password
POST /admin/create-user plugins/admin/routes.ts:458 password

Checked, for comparison: /sign-up/email (sign-up.ts:228), /change-password newPassword (update-user.ts:262), /set-password (update-user.ts:338), /reset-password (password.ts:287), /email-otp/reset-password (email-otp/routes.ts:953), /phone-number/reset-password (phone-number/routes.ts:813), /admin/set-user-password (admin/routes.ts:1723).

Expected. Every endpoint that hands a caller-supplied password to the hasher rejects it with PASSWORD_TOO_LONG when it exceeds maxPasswordLength, before any database lookup or hash, the same way sign-up.ts:228-234 does. The OWASP Password Storage Cheat Sheet requires the comparison function to have "a maximum input length, to protect against denial of service attacks with very long inputs", and better-auth's own docs present maxPasswordLength as the bound on passwords, not on new passwords only.

What version of Better Auth are you using?

1.7.4, and main at 41b7dc1 (2026-09-17).

System info

bash
Bun 1.4.2, Linux x64. Measurements below use `@better-auth/utils` 0.4.2 (the
version 1.7.4 pins), both builds.

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

typescript
Defaults. `emailAndPassword.enabled: true`, no `password.hash`/`verify`
override, no `maxPasswordLength` override.

Additional context

Measured cost with the default scrypt (N=16384, r=16, p=1), median of 15 verifyPassword calls:

Input node:crypto scrypt @noble/hashes scrypt
13 chars 38.1 ms 65.6 ms
128 chars 38.0 ms 67.1 ms
1 MiB 37.7 ms (×0.99) 73.5 ms (×1.12)
1 MiB of NFKC-expanding chars 43.0 ms (×1.13) 81.8 ms (×1.25)

scrypt's memory-hard loop does not depend on input length, so with the default hasher this is not a practical amplification. The reasons to fix it anyway:

  • The limit is a documented option and it is silently not applied on 11 of 18 password endpoints. #8899 closed on the assumption it was.
  • ctx.context.password is user-replaceable (emailAndPassword.password), and the endpoints are the only place the bound can be applied uniformly for every hasher. better-auth does not bound request body size itself; whatever the host allows reaches the hasher.
  • /admin/create-user writes a credential account with no length policy at all, so an over-long password can already exist. Once the verify-side endpoints check, such an account gets PASSWORD_TOO_LONG on sign-in. That is the same trade-off change-password already makes for newPassword and is worth a line in the changelog.