security(auth): anti-enumeration, returnTo-валидация, лимит попыток логина

Шаг 2.1 плана production-готовности:
- единый 401 при любом отказе логина + constant-time bcrypt
- forgot-password: идентичный ответ независимо от существования email
- sanitizeReturnTo (open redirect fix в /api/documents/generate-link)
- 10 неудачных попыток → блок 15 мин (in-memory, аудит)
- DATABASE_SSL_REJECT_UNAUTHORIZED env (дефолт true)
- доделка 0.7: статичные тексты в llm-providers/rag/finance-di2
- 8 новых тестов (104/104)
This commit is contained in:
2026-09-08 00:46:57 +03:00
parent 0828003141
commit 5750a3106f
15 changed files with 525 additions and 45 deletions

View File

@@ -309,3 +309,19 @@
- Как проверялось: `npm run check` чисто; vitest зелёные; lint 0 errors; `npm run build` собирается.
- Влияние на поиск/UX: нет.
- Подводные камни: dompurify используется в client/src/lib/htmlUtils.ts для rich-text — обновление патчевое, API совместим.
---
## [2.1] Средние security-фиксы
- Статус: ✅ done
- Зачем: anti-enumeration, open redirect, брутфорс, явный SSL, доделка 0.7 (Фаза 2).
- Что изменено:
- Anti-enumeration: `server/services/auth.service.ts` — bcrypt-сравнение всегда (dummy-хэш), единый 401 «Неверный email или пароль» для всех причин отказа; forgot-password — идентичный 200 «Если аккаунт существует…» независимо от email.
- returnTo: `server/utils/return-to.ts` + `client/src/lib/return-to.ts` — только относительные пути с одиночным `/` (отброс //host, /\host, ctrl-символов); применено в auth и /api/documents/generate-link (был open redirect через res.redirect).
- Лимит логина: `server/utils/login-attempts.ts` — 10 неудачных → блок 15 мин (in-memory, сброс при успехе), аудит auth.login.locked; поверх authLimiter по IP.
- SSL: `server/db.ts` — rejectUnauthorized через env DATABASE_SSL_REJECT_UNAUTHORIZED (дефолт true).
- Доделка 0.7: 200+success:false с err.message зачищены в llm-providers/rag/finance-di2.
- Как проверялось: 8 новых тестов (tests/auth-security.test.ts) — vitest 104/104; check/lint чисто.
- Влияние на поиск/UX: нет; UX логина: деактивированный аккаунт видит «Неверный email или пароль» (цель anti-enumeration), после 10 попыток — сообщение о 15-минутной блокировке.
- Подводные камни: блокировки in-memory (рестарт обнуляет; multi-instance делит лимит — как остальные кэши).