fix: auto-login types the OS password only at the logon or lock screen (#16520)

* fix: auto-login types the OS password only at the logon or lock screen

A remote-control login on Windows and macOS now reports is_logon_screen
in platform_additions, and the client skips auto-login when the peer
reports false. Peers that don't report it (older ones, Linux) keep
today's behaviour, so this narrows the leak into the focused window
without closing it: the value is a snapshot taken at login.

Windows 7 and Server 2008 R2 report the WTS lock flag reversed, so
there only LogonUI.exe tells the lock screen apart.

Refs #16510, #15261

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1grQ39ByRWPUtTBdTzEBF

* windows: drop the LogonUI.exe check from is_logon_screen

LogonUI.exe can outlive the unlock and is matched by name only, so its
presence reported a lock screen on an unlocked desktop and let the
password through. Only the WTS lock flag decides now; on Windows 7 and
Server 2008 R2, which report it reversed, the result is inverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1grQ39ByRWPUtTBdTzEBF

* windows: count only a confirmed logon or lock screen for auto-login

is_prelogin() reads a failed WTS user-name query as no user signed in,
and on Windows 7 the inverted is_locked() read a failed lock-flag query
as locked, so either failure let the password through. is_logon_screen
now uses its own queries that keep a failed read apart, and reads LOCK
and UNLOCK the other way round on Windows 7 and Server 2008 R2. The
existing functions stay as they are for their callers.

If the check task itself fails, the server reports false instead of
leaving the key out, which old clients' peers would mean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1grQ39ByRWPUtTBdTzEBF

* auto-login: ask the peer with a protobuf field instead of a JSON key

The client sets the new LoginRequest.auto_login only when Auto Login
would type the OS password, and the server runs the logon-screen check
only then, so nothing new runs for any other login. The answer is the
new PeerInfo.skip_auto_login: its default, false, is what an old or
Linux server means, so those peers keep typing as before, and neither
field is encoded unless set. The is_logon_screen key in
platform_additions is gone.

Windows is_locked() now reads the lock flag through
is_session_locked_flag(), which handles the reversed LOCK/UNLOCK of
Windows 7 and Server 2008 R2. Its only caller is the
allow-logon-screen-password check, which on those systems accepted the
permanent password on an unlocked desktop. Windows 8 and later read the
same as before. The C is_session_locked() is removed and its
WTS_CURRENT_SESSION guard moves into get_session_flags().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1grQ39ByRWPUtTBdTzEBF

* auto-login: let the peer type the password, after checking its screen

The login-time check left the whole time until the password arrived
open: the screen could be unlocked during the click, the 1.2 s wait and
two network trips, a second client's password landed on the desktop the
first one had just unlocked, and turning Auto Login on mid-login skipped
the check.

Now the client still clicks and waits, then sends the password in a new
Message.os_login (reusing OSLogin) to a Windows or macOS peer from 1.5.1.
The peer checks for its logon or lock screen right then and types the
password and Return only if it finds one; otherwise it drops them. Other
peers get the keys as before, and the OS Password menu item is
unchanged. The password goes out only after login, never in the login
request. LoginRequest.auto_login and PeerInfo.skip_auto_login are
removed, so nothing new runs unless os_login arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1grQ39ByRWPUtTBdTzEBF

* server: name os_login in scope-violation logs

A rejected os_login was logged as message.other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1grQ39ByRWPUtTBdTzEBF

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
RustDesk
2026-10-13 12:06:15 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 0b5a3d74de
commit 13e1278b4c
7 changed files with 125 additions and 8 deletions
+2
View File
@@ -1030,5 +1030,7 @@ message Message {
TerminalAction terminal_action = 31;
TerminalResponse terminal_response = 32;
PortForwardChannel port_forward_channel = 33;
// Auto Login: the peer types the password only if it is at its logon or lock screen.
OSLogin os_login = 34;
}
}
+26
View File
@@ -4467,6 +4467,32 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) {
interface.send(Data::Message(msg_out));
}
/// Auto Login. A peer that can check for its logon or lock screen gets the password and types it
/// itself, only if it is there right then. Keys typed from here land in whatever has focus if the
/// screen is not locked by the time they arrive.
pub fn auto_login(p: String, pi: &PeerInfo, interface: impl Interface) {
if !is_peer_handling_os_login(pi) {
input_os_password(p, true, interface);
return;
}
std::thread::spawn(move || {
activate_os(&interface, true);
std::thread::sleep(Duration::from_millis(1200));
let mut msg_out = Message::new();
msg_out.set_os_login(OSLogin {
password: p,
..Default::default()
});
interface.send(Data::Message(msg_out));
});
}
/// Windows and macOS peers from the 1.5.1 release handle `os_login`.
fn is_peer_handling_os_login(pi: &PeerInfo) -> bool {
(pi.platform == crate::common::PLATFORM_WINDOWS || pi.platform == crate::common::PLATFORM_MACOS)
&& hbb_common::get_version_number(&pi.version) >= hbb_common::get_version_number("1.5.1")
}
#[derive(Copy, Clone)]
struct LoginErrorMsgBox {
msgtype: &'static str,
+4
View File
@@ -827,6 +827,10 @@ pub fn is_locked() -> bool {
}
}
pub fn is_logon_screen() -> bool {
is_prelogin() || is_locked()
}
pub fn is_root() -> bool {
crate::username() == "root"
}
+7 -5
View File
@@ -604,14 +604,16 @@ extern "C"
return rdp_or_console;
}
BOOL is_session_locked(DWORD session_id)
// WTSInfoExLevel1.SessionFlags, or WTS_SESSIONSTATE_UNKNOWN when it cannot be read.
DWORD get_session_flags(DWORD session_id)
{
DWORD flags = WTS_SESSIONSTATE_UNKNOWN;
// WTS_CURRENT_SESSION, which would query the caller's session instead.
if (session_id == 0xFFFFFFFF) {
return FALSE;
return flags;
}
PWTSINFOEXW pInfo = NULL;
DWORD bytes = 0;
BOOL locked = FALSE;
if (WTSQuerySessionInformationW(
WTS_CURRENT_SERVER_HANDLE,
session_id,
@@ -619,13 +621,13 @@ extern "C"
(LPWSTR *)&pInfo,
&bytes)) {
if (pInfo && pInfo->Level == 1) {
locked = (pInfo->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_LOCK);
flags = (DWORD)pInfo->Data.WTSInfoExLevel1.SessionFlags;
}
if (pInfo) {
WTSFreeMemory(pInfo);
}
}
return locked;
return flags;
}
uint32_t get_active_user(PWSTR bufin, uint32_t nin, BOOL rdp)
+42 -2
View File
@@ -559,7 +559,6 @@ const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
extern "C" {
fn get_current_session(rdp: BOOL) -> DWORD;
fn is_session_locked(session_id: DWORD) -> BOOL;
fn LaunchProcessWin(
cmd: *const u16,
session_id: DWORD,
@@ -1263,7 +1262,7 @@ pub fn is_locked() -> bool {
let Some(session_id) = get_current_process_session_id() else {
return false;
};
unsafe { is_session_locked(session_id) == TRUE }
is_session_locked_flag(session_id) == Some(true)
}
#[inline]
@@ -1277,6 +1276,47 @@ pub fn is_logon_ui() -> ResultType<bool> {
.any(|pid| get_session_id_of_process(pid) == Some(current_sid)))
}
// Auto-login types the OS password on this, so a state that cannot be read counts as unlocked.
// `is_prelogin()` reads a failed query as no user signed in, and `is_logon_ui()` is left out
// because a LogonUI.exe can outlive the unlock.
pub fn is_logon_screen() -> bool {
let Some(session_id) = get_current_process_session_id() else {
return false;
};
is_session_prelogin(session_id) == Some(true)
|| is_session_locked_flag(session_id) == Some(true)
}
fn is_session_prelogin(session_id: u32) -> Option<bool> {
extern "C" {
fn get_session_user_info(path: *mut u16, n: u32, session_id: u32) -> u32;
}
let mut buff = [0u16; 256];
// The bytes copied include the terminating null, so 0 means the query failed.
let n = unsafe { get_session_user_info(buff.as_mut_ptr(), buff.len() as _, session_id) };
if n == 0 {
return None;
}
let username = String::from_utf16_lossy(&buff);
let username = username.trim_end_matches('\0');
Some(username.is_empty() || username == "SYSTEM")
}
// Windows 7 and Server 2008 R2 report LOCK and UNLOCK the other way round (see WTSINFOEX_LEVEL1).
fn is_session_locked_flag(session_id: u32) -> Option<bool> {
extern "C" {
fn get_session_flags(session_id: DWORD) -> DWORD;
}
const WTS_SESSIONSTATE_LOCK: DWORD = 0;
const WTS_SESSIONSTATE_UNLOCK: DWORD = 1;
let reversed = !base::platform::windows::is_windows_version_or_greater(6, 2, 0, 0, 0);
match unsafe { get_session_flags(session_id) } {
WTS_SESSIONSTATE_LOCK => Some(!reversed),
WTS_SESSIONSTATE_UNLOCK => Some(reversed),
_ => None,
}
}
pub fn is_root() -> bool {
// https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user
unsafe { is_local_system() == TRUE }
+43
View File
@@ -2463,6 +2463,41 @@ impl Connection {
self.tx_input.send(MessageInput::Key((msg, press))).ok();
}
// Auto Login: the password and Return the client would otherwise type itself, typed only if a
// check right before finds the logon or lock screen.
#[cfg(any(target_os = "windows", target_os = "macos"))]
async fn type_auto_login_password(&self, password: String) {
// A peer can send this at any rate, hence the throttled logs.
let at_logon_screen = tokio::task::spawn_blocking(crate::platform::is_logon_screen)
.await
.unwrap_or_else(|e| {
hbb_common::throttled_log!(
Duration::from_secs(60),
error,
"Failed to check the logon screen: {}",
e
);
false
});
if !at_logon_screen {
hbb_common::throttled_log!(
Duration::from_secs(60),
info,
"Auto login dropped, not at the logon or lock screen"
);
return;
}
let mut key_event = KeyEvent::new();
key_event.mode = KeyboardMode::Legacy.into();
key_event.press = true;
key_event.set_seq(password);
self.input_key(key_event.clone(), false);
key_event.set_control_key(ControlKey::Return);
CLICK_TIME.store(get_time(), Ordering::SeqCst);
MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst);
self.input_key(key_event, true);
}
fn verify_h1(&self, h1: &[u8]) -> bool {
let mut hasher2 = Sha256::new();
hasher2.update(h1);
@@ -3355,6 +3390,13 @@ impl Connection {
}
self.update_auto_disconnect_timer();
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
Some(message::Union::OsLogin(login)) => {
if self.peer_keyboard_enabled() {
self.type_auto_login_password(login.password).await;
}
self.update_auto_disconnect_timer();
}
Some(message::Union::Clipboard(cb)) => {
if self.should_handle_text_clipboard_message() && self.clipboard_enabled() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -6114,6 +6156,7 @@ impl Connection {
Some(message::Union::TerminalAction(_)) => "terminal_action",
Some(message::Union::TerminalResponse(_)) => "terminal_response",
Some(message::Union::PortForwardChannel(_)) => "port_forward_channel",
Some(message::Union::OsLogin(_)) => "os_login",
Some(message::Union::Misc(misc)) => Self::misc_message_family(misc),
Some(_) => "message.other",
None => "empty",
+1 -1
View File
@@ -1832,7 +1832,7 @@ impl<T: InvokeUiSession> Interface for Session<T> {
self.try_change_init_resolution(pi.current_display);
let p = self.lc.read().unwrap().should_auto_login();
if !p.is_empty() {
input_os_password(p, true, self.clone());
crate::client::auto_login(p, &pi, self.clone());
}
}
let current = &pi.displays[pi.current_display as usize];