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:
@@ -42,6 +42,10 @@ const SESSION_LIFETIME_DAYS = 30;
|
||||
const REMEMBER_LIFETIME_DAYS = 90;
|
||||
const RACE_TOLERANCE_MS = 300_000; // 5 минут — защита от rotation-гонок между фоновыми вкладками/PWA
|
||||
|
||||
// Фиктивный bcrypt-хэш (cost 12) для constant-time ответа при входе с
|
||||
// несуществующим email — см. anti-enumeration в login().
|
||||
const DUMMY_PASSWORD_HASH = '$2b$12$AKA6k01zovdEWcbsfyUu6eYQyakr7Z.COfTbva8MXWD4jn5u9OgQy';
|
||||
|
||||
function isSecureCookie(): boolean {
|
||||
// In production we always serve over HTTPS behind Traefik.
|
||||
return process.env.NODE_ENV === 'production';
|
||||
@@ -190,23 +194,23 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return { success: false, error: 'Неверный email или пароль' };
|
||||
}
|
||||
// Anti-enumeration: bcrypt-сравнение выполняется всегда — при
|
||||
// несуществующем пользователе с захардкоженным хэшем, чтобы по
|
||||
// времени ответа нельзя было отличить состояние аккаунта.
|
||||
// Текст ошибки единый для «не найден» / «неактивен» / «неверный пароль».
|
||||
const isValidPassword = await verifyPassword(
|
||||
data.password,
|
||||
user?.passwordHash ?? DUMMY_PASSWORD_HASH
|
||||
);
|
||||
|
||||
if (!user.isActive) {
|
||||
return { success: false, error: 'Аккаунт заблокирован' };
|
||||
if (!user || !user.isActive || !isValidPassword) {
|
||||
return { success: false, error: 'Неверный email или пароль' };
|
||||
}
|
||||
|
||||
if (user.organization && !user.organization.isActive) {
|
||||
return { success: false, error: 'Доступ организации заблокирован' };
|
||||
}
|
||||
|
||||
const isValidPassword = await verifyPassword(data.password, user.passwordHash);
|
||||
if (!isValidPassword) {
|
||||
return { success: false, error: 'Неверный email или пароль' };
|
||||
}
|
||||
|
||||
const remember = data.remember ?? false;
|
||||
const tokens = await this.createSession(user, remember, req);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user