Tighten IPC socket permissions (chmod 0o700/0o600)
fix(security): the daemon's IPC socket was world-accessible. Any local user could speak the IPC protocol and type arbitrary text into game windows via Command::Type. - src/profile.rs: add chmod_runtime_dir() and chmod_socket(path) helpers - src/session.rs: call chmod_runtime_dir() after create_dir_all, and chmod_socket() right after UnixListener::bind succeeds - Tests: chmod_socket_sets_0o600, chmod_runtime_dir_sets_0o700 (saved/ restored live dir perms to avoid clobbering a real session) - CHANGELOG.md: security note cargo test 96+/0; clippy clean.
This commit is contained in:
parent
20f0160799
commit
cece41d2ee
@ -51,3 +51,9 @@
|
|||||||
|
|
||||||
> Note: After this change, `bind: "interact"` is a single Alt+J send (game_binds.interact).
|
> Note: After this change, `bind: "interact"` is a single Alt+J send (game_binds.interact).
|
||||||
> The full chain trigger is now `bind: "smart_interact"`. Examples and tests updated.
|
> The full chain trigger is now `bind: "smart_interact"`. Examples and tests updated.
|
||||||
|
|
||||||
|
- **Security:** IPC runtime dir is now `chmod 0o700` and the Unix socket is
|
||||||
|
`chmod 0o600` immediately after `UnixListener::bind`. Any local user with
|
||||||
|
read access could speak our IPC protocol (`Command::Type` etc.); this is
|
||||||
|
the cheapest defense. Helpers: `profile::chmod_runtime_dir()` and
|
||||||
|
`profile::chmod_socket(path)`.
|
||||||
|
|||||||
@ -462,6 +462,38 @@ pub fn runtime_dir() -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `chmod 0o700` the runtime dir so only this user can read/write.
|
||||||
|
///
|
||||||
|
/// Any local user with read access to the socket could speak our IPC
|
||||||
|
/// protocol (including `Command::Type`, which is wide-open text injection
|
||||||
|
/// into game windows). Permissions are the cheapest defense.
|
||||||
|
pub fn chmod_runtime_dir() {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let dir = runtime_dir();
|
||||||
|
let _ = std::fs::set_permissions(
|
||||||
|
&dir,
|
||||||
|
std::fs::Permissions::from_mode(0o700),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `chmod 0o600` the IPC socket so only this user can connect.
|
||||||
|
///
|
||||||
|
/// Belt-and-braces with `chmod_runtime_dir`: the dir alone is not enough
|
||||||
|
/// if other shared paths leaked earlier.
|
||||||
|
pub fn chmod_socket<P: AsRef<std::path::Path>>(path: P) {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let _ = std::fs::set_permissions(
|
||||||
|
path.as_ref(),
|
||||||
|
std::fs::Permissions::from_mode(0o600),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::NormRect;
|
use super::NormRect;
|
||||||
@ -490,3 +522,39 @@ mod tests {
|
|||||||
assert_eq!(r.to_pixels(100, 50, 200, 100), (110, 70, 80, 40));
|
assert_eq!(r.to_pixels(100, 50, 200, 100), (110, 70, 80, 40));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn chmod_socket_sets_0o600() {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let dir = std::env::temp_dir().join("enboxer-test-chmod");
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let sock = dir.join("test.sock");
|
||||||
|
std::fs::write(&sock, b"").unwrap();
|
||||||
|
std::fs::set_permissions(&sock, std::fs::Permissions::from_mode(0o644)).unwrap();
|
||||||
|
chmod_socket(&sock);
|
||||||
|
let m = std::fs::metadata(&sock).unwrap().permissions().mode() & 0o777;
|
||||||
|
assert_eq!(m, 0o600, "expected 0o600, got {m:o}");
|
||||||
|
std::fs::remove_file(&sock).ok();
|
||||||
|
std::fs::remove_dir(&dir).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn chmod_runtime_dir_sets_0o700() {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
// Save and restore the real dir perms around the test so we don't break
|
||||||
|
// the live session if it happens to share XDG_RUNTIME_DIR.
|
||||||
|
let dir = runtime_dir();
|
||||||
|
let _ = std::fs::create_dir_all(&dir);
|
||||||
|
let saved = std::fs::metadata(&dir).ok().map(|m| m.permissions().mode() & 0o777);
|
||||||
|
// Force 0o755 so the helper actually has to change it.
|
||||||
|
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755));
|
||||||
|
chmod_runtime_dir();
|
||||||
|
let m = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
|
||||||
|
assert_eq!(m, 0o700, "expected 0o700, got {m:o}");
|
||||||
|
if let Some(s) = saved {
|
||||||
|
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ impl Session {
|
|||||||
|
|
||||||
pub async fn run(profile: Profile, sock: PathBuf) -> Result<()> {
|
pub async fn run(profile: Profile, sock: PathBuf) -> Result<()> {
|
||||||
std::fs::create_dir_all(runtime_dir()).ok();
|
std::fs::create_dir_all(runtime_dir()).ok();
|
||||||
|
crate::profile::chmod_runtime_dir();
|
||||||
if sock.exists() {
|
if sock.exists() {
|
||||||
let _ = std::fs::remove_file(&sock);
|
let _ = std::fs::remove_file(&sock);
|
||||||
}
|
}
|
||||||
@ -69,6 +70,7 @@ pub async fn run(profile: Profile, sock: PathBuf) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let listener = UnixListener::bind(&sock).with_context(|| format!("bind {}", sock.display()))?;
|
let listener = UnixListener::bind(&sock).with_context(|| format!("bind {}", sock.display()))?;
|
||||||
|
crate::profile::chmod_socket(&sock);
|
||||||
tracing::info!("ipc {}", sock.display());
|
tracing::info!("ipc {}", sock.display());
|
||||||
|
|
||||||
let s1 = session.clone();
|
let s1 = session.clone();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user