Commit Graph

9 Commits

Author SHA1 Message Date
Hartmut e01074926e security: reject common/weak passwords on every set-password path (#31)
CI / Architecture Guardrails (pull_request) Successful in 6m31s
CI / Typecheck (pull_request) Failing after 6m9s
CI / Build (pull_request) Has been skipped
CI / E2E Tests (pull_request) Has been skipped
CI / Fresh-Linux Docker Deploy (pull_request) Has been skipped
CI / Assistant Split Regression (pull_request) Successful in 7m23s
CI / Lint (pull_request) Successful in 6m54s
CI / Unit Tests (pull_request) Successful in 9m28s
CI / Release Images (pull_request) Has been skipped
Adds a synchronous policy check that blocks (1) the curated >=12-char
common-password list (rockyou top, predictable seasonal, admin defaults),
(2) trivial patterns (single-char repeat, short-pattern repeat, keyboard
or numeric sequences), and (3) passwords containing the user's email
local-part or any name component. Wired into all five password-mutation
sites: first-admin setup, admin createUser/setUserPassword, invite
acceptance, and password-reset.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 14:09:38 +02:00
Hartmut 01c45d0344 security: align client password policy with server, enforce AUTH_SECRET length + entropy (#56)
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 <noreply@anthropic.com>
2026-04-17 14:56:43 +02:00
Hartmut 3c5d1d37f7 security: rate-limit IP-keyed, fail-closed on empty key (#37)
Rate-limiter now accepts string | string[] so callers can key on
multiple buckets simultaneously. If any bucket is exhausted the
request is denied, which lets login/TOTP/reset-password throttle on
BOTH user identifier and source IP without either becoming a bypass.

Fail-closed: empty/whitespace-only keys now deny by default instead
of silently allowing unbounded attempts (was CWE-307 gap).

Degraded-fallback divisor reduced from /10 to /2 — the old aggressive
clamp forced-logged-out legitimate users during brief Redis outages;
/2 still meaningfully slows distributed brute-force.

Callers updated:
- auth.ts (login): both email: and ip: buckets
- auth router requestPasswordReset: email + IP
- auth router resetPassword: IP before lookup, email-reset after
- invite router getInvite/acceptInvite: IP
- user-self-service verifyTotp: userId + IP

TRPCContext now carries clientIp; web tRPC route extracts it from
X-Forwarded-For / X-Real-IP.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 08:19:33 +02:00
Hartmut 534945f6e3 security: bound password inputs, configure pino redact, patch deps (#36 #46 #58)
#36 CRITICAL: add .max(128) to all password Zod schemas to prevent
Argon2-based DoS from unbounded password strings.

#46 HIGH: configure pino redact paths so passwords/tokens/cookies/TOTP
secrets are never serialized in logs.

#58 MEDIUM: upgrade dompurify to ^3.4.0 and add pnpm overrides for
brace-expansion (>=5.0.5) and esbuild (>=0.25.0) to patch known CVEs.
Vite moderate (path traversal, dev-only) remains — requires vitest 3.x
major upgrade, deferred.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 08:13:25 +02:00
Hartmut 110e4ff1aa fix(security): harden auth reset, rate limiter fallback, and CI secrets
- Move CI_AUTH_SECRET from plaintext to ${{ secrets.CI_AUTH_SECRET }}
- Wrap password reset (update + session kill + token mark) in $transaction
  to prevent stale sessions on partial failure (CWE-613)
- Rate limiter Redis fallback now uses stricter degraded limits
  (maxRequests/10) and logs at error level instead of warn

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 08:03:42 +02:00
Hartmut 97cfd0ed90 fix(security): raise password minimum to 12 chars, hide raw error messages, add audit script
- Password validation: min(8) → min(12) across auth.ts, user-procedure-support.ts,
  and invite.ts (aligns with NIST SP 800-63B modern recommendations)
- Error boundary: stop rendering raw error.message which could leak internal
  details; always show the generic fallback text
- Add `pnpm audit` script (--audit-level=high) for dependency vulnerability scanning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:48:51 +02:00
Hartmut 3452464809 fix(security): invalidate sessions on password change and remove hash from permission API responses
- setUserPassword and resetPassword now call activeSession.deleteMany after
  updating the passwordHash, so any pre-change sessions are immediately revoked
  (CWE-613 session fixation after credential change)
- setUserPermissions and resetUserPermissions now use explicit Prisma select to
  exclude passwordHash and totpSecret from the returned user object

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:37:56 +02:00
Hartmut dc5bbdc47d feat: centralize app base URL — no localhost fallback in production
Introduce getAppBaseUrl() in packages/api/src/lib/app-base-url.ts:
- Reads NEXTAUTH_URL (trimmed, trailing slash stripped)
- production: throws if NEXTAUTH_URL is missing/empty so broken
  localhost links in emails are caught at runtime, not silently sent
- development/test: falls back to http://localhost:3100 with a
  one-time console.warn

Replace the duplicated inline fallback in:
- packages/api/src/router/invite.ts (invite email link)
- packages/api/src/router/auth.ts (password reset email link)

Extend GET /api/health to report:
  "baseUrl": { "configured": bool, "isLocalhost": bool }
so deployment checks can detect a misconfigured NEXTAUTH_URL.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-02 14:19:19 +02:00
Hartmut fceceeee4b feat: SMTP full ENV override, password reset flow, and E2E email testing
- SMTP: SMTP_HOST/PORT/USER/FROM/TLS now all have ENV override support
  (previously only SMTP_PASSWORD was env-aware). ENV takes priority over DB.
- docker-compose.yml: forward all SMTP_* env vars to app container + add
  Mailhog service (ports 1025 SMTP / 8025 HTTP, always available in dev)
- Password reset: PasswordResetToken Prisma model + authRouter with
  requestPasswordReset (timing-safe, no email enumeration) + resetPassword
- UI: /auth/forgot-password, /auth/reset-password/[token] pages +
  "Forgot password?" link on sign-in page
- E2E: Mailhog helpers (getLatestEmailTo, clearMailhog, extractUrlFromEmail)
  + invite-flow.spec.ts + password-reset.spec.ts

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-02 08:55:39 +02:00