diff --git a/src/server/varlink_api/walld.rs b/src/server/varlink_api/walld.rs index 31c5a77..6bc9bd8 100644 --- a/src/server/varlink_api/walld.rs +++ b/src/server/varlink_api/walld.rs @@ -148,6 +148,22 @@ fn hostname() -> String { .unwrap_or_else(|| "unknown".to_string()) } +/// Verifies that `source_tty` (if given) belongs to `source_uid`. +fn verify_source_tty( + source_tty: Option<&str>, + source_uid: u32, +) -> Result<(), VarlinkWalldClientError> { + let Some(tty) = source_tty else { + return Ok(()); + }; + + tty_utils::verify_tty_owner(tty, source_uid).map_err(|err| { + VarlinkWalldClientError::InvalidRequest { + message: format!("source_tty {tty:?}: {err}"), + } + }) +} + fn tty_error_to_walld_error(user: &str, tty: &str, err: TtyError) -> VarlinkWalldClientError { match err { TtyError::NotFound(_) | TtyError::InvalidName(_) | TtyError::NotCharacterDevice(_) => { @@ -239,6 +255,8 @@ pub async fn handle_wall_request( }); } + verify_source_tty(source_tty.as_deref(), source_uid)?; + authorize(polkit, |authority| { check_wall(authority, source_pid, group.as_deref()) }) @@ -284,6 +302,8 @@ pub async fn handle_write_request( tty: Option, message: String, ) -> Result { + verify_source_tty(source_tty.as_deref(), source_uid)?; + authorize(polkit, |authority| { check_write(authority, source_pid, &user, tty.as_deref()) }) diff --git a/src/server/walld/tty_utils.rs b/src/server/walld/tty_utils.rs index 0a5780f..a1b5b6a 100644 --- a/src/server/walld/tty_utils.rs +++ b/src/server/walld/tty_utils.rs @@ -33,6 +33,8 @@ pub enum TtyError { Unavailable(String), #[error("timed out writing to {0}")] Timeout(String), + #[error("{0}: not owned by the caller")] + NotOwnedByCaller(String), #[error("{path}: {source}")] Io { path: String, @@ -89,6 +91,32 @@ pub fn stat_tty(name: &str) -> Result { }) } +/// Verifies that `path` is a character device in `/dev/` owned by `uid`. +pub fn verify_tty_owner(path: &str, uid: u32) -> Result<(), TtyError> { + if !path.starts_with("/dev/") || path.split('/').any(|part| part == "..") { + return Err(TtyError::InvalidName(path.to_string())); + } + + let st = stat(Path::new(path)).map_err(|e| match e { + Errno::ENOENT => TtyError::NotFound(path.to_string()), + other => TtyError::Io { + path: path.to_string(), + source: other.into(), + }, + })?; + + let file_type = SFlag::from_bits_truncate(st.st_mode) & SFlag::S_IFMT; + if file_type != SFlag::S_IFCHR { + return Err(TtyError::NotCharacterDevice(path.to_string())); + } + + if st.st_uid != uid { + return Err(TtyError::NotOwnedByCaller(path.to_string())); + } + + Ok(()) +} + /// A single logged-in session, as reported by utmpx (systemd-logind) #[derive(Debug, Clone, PartialEq, Eq)] pub struct Session {