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 <sunboeasy@gmail.com>

* replace LazyLock with lazy_static

Signed-off-by: 21pages <sunboeasy@gmail.com>

* read temporary password after locking failure state

Signed-off-by: 21pages <sunboeasy@gmail.com>

* server: rotate temporary passwords after 10 consecutive failures

Signed-off-by: 21pages <sunboeasy@gmail.com>

* server: clarify temporary password failure counter comment

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages 2026-04-09 17:14:21 +08:00 committed by GitHub
parent 0cf3e8ed40
commit 8dea347a21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<State> =
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;