fix(peer): fail instead of falling back to /tmp for the state directory
Security audit finding EXP2-SEC-06. When no explicit state directory, `LANSPREAD_STATE_DIR`, `HOME` or `USERPROFILE` was available, `resolve_state_dir` silently used `<temp_dir>/lanspread`. On a multi-user machine that is a predictable, world-writable location: another local user could pre-create it and then read or replace the Ed25519 peer identity and the download ownership journals stored there. Both shipping callers always provide a directory (the Tauri app passes its app-data path, the CLI its `--state-dir`), so the fallback was only reachable in unusual environments. Rather than derive a UID-specific temp path, peer startup now returns an error naming the accepted sources. This is the same fail-closed stance the codebase already takes for a malformed sharing policy. Test plan: `just test`. Manually, `LANSPREAD_STATE_DIR= HOME= lanspread-peer-cli ...` without `--state-dir` must refuse to start with a clear message; normal `just run` and `just peer-cli-run` are unaffected. Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
This commit is contained in:
@@ -595,7 +595,7 @@ pub fn start_peer_with_options(
|
|||||||
stream_install_provider,
|
stream_install_provider,
|
||||||
local_network_sharing,
|
local_network_sharing,
|
||||||
} = options;
|
} = options;
|
||||||
let state_dir = resolve_state_dir(state_dir.as_deref());
|
let state_dir = resolve_state_dir(state_dir.as_deref())?;
|
||||||
let requested_game_dir = game_dir.into();
|
let requested_game_dir = game_dir.into();
|
||||||
let game_dir = canonicalize_game_dir(&requested_game_dir)?;
|
let game_dir = canonicalize_game_dir(&requested_game_dir)?;
|
||||||
install::intent::scan_active_install_intents(&state_dir, &game_dir)?;
|
install::intent::scan_active_install_intents(&state_dir, &game_dir)?;
|
||||||
|
|||||||
@@ -15,20 +15,34 @@ pub(crate) const LEGACY_DOWNLOAD_OWNERSHIP_TMP_FILE: &str = "download_ownership.
|
|||||||
pub(crate) const LEGACY_DOWNLOAD_OWNERSHIP_RECOVERY_REQUIRED_FILE: &str =
|
pub(crate) const LEGACY_DOWNLOAD_OWNERSHIP_RECOVERY_REQUIRED_FILE: &str =
|
||||||
"download_ownership.recovery-required";
|
"download_ownership.recovery-required";
|
||||||
|
|
||||||
pub(crate) fn resolve_state_dir(explicit: Option<&Path>) -> PathBuf {
|
/// Resolves the directory that holds the peer identity, ownership journals and
|
||||||
|
/// other durable state.
|
||||||
|
///
|
||||||
|
/// Precedence: an explicit path from the embedding application (the Tauri
|
||||||
|
/// app passes its app-data directory, the CLI its `--state-dir`), then
|
||||||
|
/// `LANSPREAD_STATE_DIR`, then `$HOME`/`%USERPROFILE%/.lanspread`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when none of these sources is available. State holds the
|
||||||
|
/// private identity key, so it is never placed in a shared, world-writable
|
||||||
|
/// temporary directory where another local user could pre-create it.
|
||||||
|
pub(crate) fn resolve_state_dir(explicit: Option<&Path>) -> eyre::Result<PathBuf> {
|
||||||
if let Some(dir) = explicit {
|
if let Some(dir) = explicit {
|
||||||
return dir.to_path_buf();
|
return Ok(dir.to_path_buf());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dir) = std::env::var_os("LANSPREAD_STATE_DIR") {
|
if let Some(dir) = std::env::var_os("LANSPREAD_STATE_DIR") {
|
||||||
return PathBuf::from(dir);
|
return Ok(PathBuf::from(dir));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
|
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
|
||||||
return PathBuf::from(home).join(".lanspread");
|
return Ok(PathBuf::from(home).join(".lanspread"));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::env::temp_dir().join("lanspread")
|
eyre::bail!(
|
||||||
|
"no state directory: pass one explicitly or set LANSPREAD_STATE_DIR, HOME, or USERPROFILE"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn peer_identity_path(state_dir: &Path) -> PathBuf {
|
pub(crate) fn peer_identity_path(state_dir: &Path) -> PathBuf {
|
||||||
@@ -161,3 +175,17 @@ fn hex_encode(bytes: &[u8]) -> String {
|
|||||||
}
|
}
|
||||||
encoded
|
encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_state_dir_takes_precedence() {
|
||||||
|
let explicit = Path::new("/explicit/state");
|
||||||
|
assert_eq!(
|
||||||
|
resolve_state_dir(Some(explicit)).expect("explicit path resolves"),
|
||||||
|
explicit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user