From 8dea347a216f6b34a5b53f67e5ac93eba3d731f7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 9 Apr 2026 17:14:21 +0800 Subject: [PATCH] add brute-force protection for one-time password (#14682) * add brute-force protection for temporary password Rotate the temporary password after repeated failed login attempts within one minute, and reset the failure window after successful authentication. Signed-off-by: 21pages * replace LazyLock with lazy_static Signed-off-by: 21pages * read temporary password after locking failure state Signed-off-by: 21pages * server: rotate temporary passwords after 10 consecutive failures Signed-off-by: 21pages * server: clarify temporary password failure counter comment Signed-off-by: 21pages --------- Signed-off-by: 21pages --- src/server/connection.rs | 61 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 0e7f26263..8b4eb0c48 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1993,11 +1993,6 @@ impl Connection { constant_time_eq(&hasher2.finalize()[..], &self.lr.password[..]) } - #[inline] - fn validate_one_password(&self, password: &str) -> bool { - self.validate_password_plain(password) - } - fn validate_password_plain(&self, password: &str) -> bool { if password.is_empty() { return false; @@ -2025,15 +2020,68 @@ impl Connection { self.validate_password_plain(storage) } + // This is coarse brute-force protection for the current temporary password value. + // We only care whether the active temporary password itself was presented correctly, + // not whether later authorization steps succeed. A successful temporary-password + // match clears this state immediately, and the counter also resets whenever the + // temporary password changes or is rotated. + fn check_update_temporary_password(&self, temporary_password_success: bool) { + const MAX_CONSECUTIVE_FAILURES: i32 = 10; + #[derive(Default)] + struct State { + password: String, + failures: i32, + } + lazy_static::lazy_static! { + static ref TEMPORARY_PASSWORD_FAILURES: Mutex = + Mutex::new(State::default()); + } + + if !password::temporary_enabled() { + return; + } + + let mut state = TEMPORARY_PASSWORD_FAILURES.lock().unwrap(); + let current_password = password::temporary_password(); + if current_password.is_empty() { + return; + } + if state.password != current_password { + state.password = current_password; + state.failures = 0; + } + + if temporary_password_success { + state.failures = 0; + return; + } + state.failures += 1; + + if state.failures < MAX_CONSECUTIVE_FAILURES { + return; + } + + password::update_temporary_password(); + let new_password = password::temporary_password(); + log::warn!( + "Temporary password rotated after too many consecutive wrong attempts: failures={}, ip={}", + state.failures, + self.ip, + ); + state.password = new_password; + state.failures = 0; + } + fn validate_password(&mut self, allow_permanent_password: bool) -> bool { if password::temporary_enabled() { let password = password::temporary_password(); - if self.validate_one_password(&password) { + if self.validate_password_plain(&password) { raii::AuthedConnID::update_or_insert_session( self.session_key(), Some(password), Some(false), ); + self.check_update_temporary_password(true); return true; } } @@ -2406,6 +2454,7 @@ impl Connection { } if !self.validate_password(allow_logon_screen_password) { self.update_failure(failure, false, 0); + self.check_update_temporary_password(false); if err_msg.is_empty() { self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) .await;