From 01c45d0344946be07c967344c14ec3d95f20e375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Fri, 17 Apr 2026 14:56:43 +0200 Subject: [PATCH] security: align client password policy with server, enforce AUTH_SECRET length + entropy (#56) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Client-side validators (reset-password, invite-accept, first-admin setup, user-create modal) previously checked password.length < 8 while every server-side Zod schema required .min(12). External API consumers (or a confused browser UI) could get past the client check but fail at the tRPC boundary — or worse, quietly under-enforce policy compared to what admins expect. Fix: introduce PASSWORD_MIN_LENGTH (12) and PASSWORD_MAX_LENGTH (128) in @capakraken/shared and import them from every pre-submit client validator and every server Zod schema. Single source of truth; drift becomes a compile error rather than a security finding. Also hardens the AUTH_SECRET runtime check: in addition to the existing placeholder-blacklist, production startup now rejects secrets shorter than 32 chars OR with Shannon entropy below 3.5 bits/char. That covers low-entropy-but-long values like "aaaa..." (38 chars, entropy 0) which would have passed the previous checks. Documented the rotation process for AUTH_SECRET + POSTGRES_PASSWORD in docs/security-architecture.md §3. Verified: - pnpm test:unit — 396 files / 1922 tests passed - pnpm --filter @capakraken/web exec tsc --noEmit — clean - pnpm --filter @capakraken/api exec tsc --noEmit — clean Co-Authored-By: Claude Opus 4.7 --- .../app/auth/reset-password/[token]/page.tsx | 21 +++++------- apps/web/src/app/invite/[token]/page.tsx | 31 +++++++++++------ apps/web/src/app/setup/SetupClient.tsx | 13 ++++--- apps/web/src/app/setup/actions.ts | 15 ++++++-- .../src/components/admin/UserCreateModal.tsx | 7 ++-- apps/web/src/server/runtime-env.test.ts | 26 ++++++++++++-- apps/web/src/server/runtime-env.ts | 34 +++++++++++++++++++ docs/security-architecture.md | 14 +++++++- packages/api/src/router/auth.ts | 10 +++++- packages/api/src/router/invite.ts | 10 +++++- .../api/src/router/user-procedure-support.ts | 9 +++-- packages/shared/src/constants/index.ts | 17 ++++++++-- 12 files changed, 163 insertions(+), 44 deletions(-) diff --git a/apps/web/src/app/auth/reset-password/[token]/page.tsx b/apps/web/src/app/auth/reset-password/[token]/page.tsx index c7589e2..e7716dc 100644 --- a/apps/web/src/app/auth/reset-password/[token]/page.tsx +++ b/apps/web/src/app/auth/reset-password/[token]/page.tsx @@ -2,6 +2,7 @@ import { use, useState } from "react"; import { useRouter } from "next/navigation"; +import { PASSWORD_MIN_LENGTH, PASSWORD_POLICY_MESSAGE } from "@capakraken/shared"; import { trpc } from "~/lib/trpc/client.js"; export default function ResetPasswordPage({ params }: { params: Promise<{ token: string }> }) { @@ -21,8 +22,8 @@ export default function ResetPasswordPage({ params }: { params: Promise<{ token: function handleSubmit(e: React.FormEvent) { e.preventDefault(); setFormError(null); - if (password.length < 8) { - setFormError("Password must be at least 8 characters."); + if (password.length < PASSWORD_MIN_LENGTH) { + setFormError(PASSWORD_POLICY_MESSAGE); return; } if (password !== confirm) { @@ -40,9 +41,7 @@ export default function ResetPasswordPage({ params }: { params: Promise<{ token:

Password updated

-

- Your password has been changed successfully. -

+

Your password has been changed successfully.