From 711d032698929fbd55d327d4b3d88d8062bc753a Mon Sep 17 00:00:00 2001 From: Drumee Dev Date: Sun, 27 Sep 2026 19:05:17 -0700 Subject: [PATCH] fix(signup): enforce the password policy on create_account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rules (8+ chars, uppercase, number, symbol) lived only in the signup UI, so a direct call to signup.create_account could set any password. Checked on the trimmed value — the one that is hashed and that login compares — and rejected with {status: "weak_password", missing: [keys]}, keys matching the UI's PW_NEEDS_* locale keys. Co-Authored-By: Claude Opus 5.5 (1M context) --- service/signup.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/service/signup.js b/service/signup.js index f3ccff0..7f93d0e 100644 --- a/service/signup.js +++ b/service/signup.js @@ -23,6 +23,18 @@ const { isEmpty } = require("lodash"); const { notifyMemberJoined } = require("./lib/notify-member-joined"); const { Mfs } = require("@drumee/server-core"); +// Password policy — KEEP IN STEP with PW_RULES in the signup UI +// (signup/src/widgets/form/index.js). The UI checks first for the inline +// message; this is the authoritative check, since the endpoint can be called +// directly. Keys double as the UI's LOCALE keys for the "still needs" list. +const PW_SPECIALS = /[\[\]\{\}\'\"\ \-\_\+\=\|\!\:\;\,\?\.\/\*\%\$\&\#\(\)\@]/; +const PW_RULES = [ + { key: "PW_NEEDS_MIN", test: (v) => v.length >= 8 }, + { key: "PW_NEEDS_UPPERCASE", test: (v) => /[A-Z]/.test(v) }, + { key: "PW_NEEDS_NUMBER", test: (v) => /[0-9]/.test(v) }, + { key: "PW_NEEDS_SYMBOL", test: (v) => PW_SPECIALS.test(v) }, +]; + class __signup extends Mfs { /** @@ -34,6 +46,13 @@ class __signup extends Mfs { const email = this.input.need(Attr.email).trim(); const password = this.input.need(Attr.password).trim(); + // Checked on the TRIMMED value — the one that is hashed and that login + // (yp.login, session.signin) compares against. + const missing = PW_RULES.filter((r) => !r.test(password)).map((r) => r.key); + if (missing.length) { + return this.output.data({ status: "weak_password", missing }); + } + const existingUser = await this.yp.await_proc("drumate_exists", email); if (existingUser && existingUser.email) { return this.output.data({ status: "user_exists", email });