mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-19 19:01:05 +03:00
harden login request retry
This commit is contained in:
@@ -363,6 +363,9 @@ pub struct Connection {
|
|||||||
server_audit_file: String,
|
server_audit_file: String,
|
||||||
controlled_context: Option<ControlledContext>,
|
controlled_context: Option<ControlledContext>,
|
||||||
lr: LoginRequest,
|
lr: LoginRequest,
|
||||||
|
// Authentication retries may update credentials, but not the requested session scope.
|
||||||
|
// A digest, so no peer-controlled strings are retained.
|
||||||
|
login_scope: Option<[u8; 32]>,
|
||||||
peer_argb: u32,
|
peer_argb: u32,
|
||||||
session_last_recv_time: Option<Arc<Mutex<Instant>>>,
|
session_last_recv_time: Option<Arc<Mutex<Instant>>>,
|
||||||
chat_unanswered: bool,
|
chat_unanswered: bool,
|
||||||
@@ -560,6 +563,7 @@ impl Connection {
|
|||||||
server_audit_file: "".to_owned(),
|
server_audit_file: "".to_owned(),
|
||||||
controlled_context,
|
controlled_context,
|
||||||
lr: Default::default(),
|
lr: Default::default(),
|
||||||
|
login_scope: None,
|
||||||
peer_argb: 0u32,
|
peer_argb: 0u32,
|
||||||
session_last_recv_time: None,
|
session_last_recv_time: None,
|
||||||
chat_unanswered: false,
|
chat_unanswered: false,
|
||||||
@@ -2621,6 +2625,90 @@ impl Connection {
|
|||||||
self.terminal_persistent = false;
|
self.terminal_persistent = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Approval and whitelist decisions must stay bound to the same controller identity and
|
||||||
|
// session scope across authentication retries.
|
||||||
|
fn login_scope_digest(lr: &LoginRequest) -> [u8; 32] {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
// Length-prefixed so adjacent fields cannot alias.
|
||||||
|
let mut push = |bytes: &[u8]| {
|
||||||
|
hasher.update((bytes.len() as u64).to_le_bytes());
|
||||||
|
hasher.update(bytes);
|
||||||
|
};
|
||||||
|
push(lr.my_id.as_bytes());
|
||||||
|
// Payloads are destructured exhaustively: a new field fails to compile until it is
|
||||||
|
// either latched here or deliberately ignored.
|
||||||
|
match lr.union.as_ref() {
|
||||||
|
Some(login_request::Union::FileTransfer(ft)) => {
|
||||||
|
let FileTransfer {
|
||||||
|
dir,
|
||||||
|
show_hidden,
|
||||||
|
special_fields: _,
|
||||||
|
} = ft;
|
||||||
|
push(b"file_transfer");
|
||||||
|
push(dir.as_bytes());
|
||||||
|
push(&[*show_hidden as u8]);
|
||||||
|
}
|
||||||
|
Some(login_request::Union::ViewCamera(vc)) => {
|
||||||
|
let ViewCamera { special_fields: _ } = vc;
|
||||||
|
push(b"view_camera");
|
||||||
|
}
|
||||||
|
Some(login_request::Union::Terminal(t)) => {
|
||||||
|
let Terminal {
|
||||||
|
service_id,
|
||||||
|
special_fields: _,
|
||||||
|
} = t;
|
||||||
|
push(b"terminal");
|
||||||
|
push(service_id.as_bytes());
|
||||||
|
}
|
||||||
|
Some(login_request::Union::PortForward(pf)) => {
|
||||||
|
let PortForward {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
special_fields: _,
|
||||||
|
} = pf;
|
||||||
|
push(b"port_forward");
|
||||||
|
push(host.as_bytes());
|
||||||
|
push(&port.to_le_bytes());
|
||||||
|
}
|
||||||
|
// Variants this build does not know execute as remote, so they latch as remote.
|
||||||
|
None | Some(_) => push(b"remote"),
|
||||||
|
}
|
||||||
|
hasher.finalize().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging only; security decisions compare digests.
|
||||||
|
fn login_scope_kind(lr: &LoginRequest) -> &'static str {
|
||||||
|
match lr.union.as_ref() {
|
||||||
|
Some(login_request::Union::FileTransfer(_)) => "file_transfer",
|
||||||
|
Some(login_request::Union::ViewCamera(_)) => "view_camera",
|
||||||
|
Some(login_request::Union::Terminal(_)) => "terminal",
|
||||||
|
Some(login_request::Union::PortForward(_)) => "port_forward",
|
||||||
|
_ => "remote",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_login_scope(&mut self, lr: &LoginRequest) -> bool {
|
||||||
|
let requested = Self::login_scope_digest(lr);
|
||||||
|
match self.login_scope {
|
||||||
|
Some(initial) if initial != requested => {
|
||||||
|
// self.lr still holds the first accepted request, whose scope is the latched one.
|
||||||
|
log::warn!(
|
||||||
|
"Rejected login scope change: conn_id={}, initial={}, requested={}",
|
||||||
|
self.inner.id(),
|
||||||
|
Self::login_scope_kind(&self.lr),
|
||||||
|
Self::login_scope_kind(lr),
|
||||||
|
);
|
||||||
|
self.send_login_error("Connection not allowed").await;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Some(_) => true,
|
||||||
|
None => {
|
||||||
|
self.login_scope = Some(requested);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) {
|
async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) {
|
||||||
self.lr = lr.clone();
|
self.lr = lr.clone();
|
||||||
self.peer_argb = crate::str2color(&format!("{}{}", &lr.my_id, &lr.my_platform), 0xff);
|
self.peer_argb = crate::str2color(&format!("{}{}", &lr.my_id, &lr.my_platform), 0xff);
|
||||||
@@ -2698,6 +2786,9 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
// After handling CloseReason messages, proceed to process other message types
|
// After handling CloseReason messages, proceed to process other message types
|
||||||
if let Some(message::Union::LoginRequest(lr)) = msg.union {
|
if let Some(message::Union::LoginRequest(lr)) = msg.union {
|
||||||
|
if !self.check_login_scope(&lr).await {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
self.awaiting_2fa = false;
|
self.awaiting_2fa = false;
|
||||||
self.handle_login_request_without_validation(&lr).await;
|
self.handle_login_request_without_validation(&lr).await;
|
||||||
if self.authorized {
|
if self.authorized {
|
||||||
@@ -7003,6 +7094,59 @@ mod test {
|
|||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login_scope_latches_session_scope_across_login_retries() {
|
||||||
|
let port_forward = |host: &str| {
|
||||||
|
let mut lr = LoginRequest::new();
|
||||||
|
lr.my_id = "peer".to_owned();
|
||||||
|
lr.set_port_forward(PortForward {
|
||||||
|
host: host.to_owned(),
|
||||||
|
port: 3389,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
lr
|
||||||
|
};
|
||||||
|
let first = port_forward("localhost");
|
||||||
|
let scope = |lr: &LoginRequest| Connection::login_scope_digest(lr);
|
||||||
|
|
||||||
|
// A retry may carry new credentials, profile data, options, and unknown fields.
|
||||||
|
let mut retry = port_forward("localhost");
|
||||||
|
retry.password = "secret".into();
|
||||||
|
retry.hwid = "hwid".into();
|
||||||
|
retry.os_login = Some(OSLogin {
|
||||||
|
username: "admin".to_owned(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into();
|
||||||
|
retry.my_name = "New Display Name".to_owned();
|
||||||
|
retry.avatar = "data:image/png;base64,AAAA".to_owned();
|
||||||
|
retry
|
||||||
|
.special_fields
|
||||||
|
.mut_unknown_fields()
|
||||||
|
.add_varint(9999, 1);
|
||||||
|
assert_eq!(scope(&first), scope(&retry));
|
||||||
|
|
||||||
|
// It may not change the controller identity, move the target, or switch type.
|
||||||
|
let mut rotated_id = first.clone();
|
||||||
|
rotated_id.my_id = "rotated-id".to_owned();
|
||||||
|
assert_ne!(scope(&first), scope(&rotated_id));
|
||||||
|
assert_ne!(scope(&first), scope(&port_forward("10.0.0.5")));
|
||||||
|
let mut moved_port = port_forward("localhost");
|
||||||
|
moved_port.mut_port_forward().port = 22;
|
||||||
|
assert_ne!(scope(&first), scope(&moved_port));
|
||||||
|
let terminal = |service_id: &str| {
|
||||||
|
let mut lr = LoginRequest::new();
|
||||||
|
lr.my_id = "peer".to_owned();
|
||||||
|
lr.set_terminal(Terminal {
|
||||||
|
service_id: service_id.to_owned(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
lr
|
||||||
|
};
|
||||||
|
assert_ne!(scope(&first), scope(&terminal("")));
|
||||||
|
assert_ne!(scope(&terminal("a")), scope(&terminal("b")));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_wildcard_match() {
|
fn test_wildcard_match() {
|
||||||
// Exact match.
|
// Exact match.
|
||||||
|
|||||||
Reference in New Issue
Block a user