mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-09 10:49:29 +00:00
fix(security): harden credential and path boundaries
Preserve pending pairing tokens until credential-store migration succeeds, defer keychain access until frontend hydration, reject symlink and malformed ownership paths, and restrict metadata credentials to exact origins. Refs #15 Refs #16
This commit is contained in:
+96
-15
@@ -55,31 +55,38 @@ impl StorageLayout {
|
||||
app_handle: &AppHandle<R>,
|
||||
mode: StorageMode,
|
||||
) -> Result<Self, String> {
|
||||
match mode {
|
||||
StorageMode::Standard => Ok(Self {
|
||||
mode: StorageMode::Standard,
|
||||
data_dir: app_handle
|
||||
let (mode, data_dir, log_dir, webview_dir) = match mode {
|
||||
StorageMode::Standard => (
|
||||
StorageMode::Standard,
|
||||
app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("failed to resolve app data directory: {error}"))?,
|
||||
log_dir: app_handle
|
||||
app_handle
|
||||
.path()
|
||||
.app_log_dir()
|
||||
.map_err(|error| format!("failed to resolve app log directory: {error}"))?,
|
||||
webview_dir: app_handle.path().app_local_data_dir().map_err(|error| {
|
||||
app_handle.path().app_local_data_dir().map_err(|error| {
|
||||
format!("failed to resolve app local data directory: {error}")
|
||||
})?,
|
||||
}),
|
||||
),
|
||||
StorageMode::Portable { root } => {
|
||||
let data_dir = root.join(PORTABLE_DATA_DIR);
|
||||
Ok(Self {
|
||||
mode: StorageMode::Portable { root },
|
||||
log_dir: data_dir.join(PORTABLE_LOG_DIR),
|
||||
webview_dir: data_dir.join(PORTABLE_WEBVIEW_DIR),
|
||||
data_dir,
|
||||
})
|
||||
(
|
||||
StorageMode::Portable { root },
|
||||
data_dir.clone(),
|
||||
data_dir.join(PORTABLE_LOG_DIR),
|
||||
data_dir.join(PORTABLE_WEBVIEW_DIR),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
mode,
|
||||
data_dir: canonicalize_storage_path(&data_dir)?,
|
||||
log_dir: canonicalize_storage_path(&log_dir)?,
|
||||
webview_dir: canonicalize_storage_path(&webview_dir)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_portable(&self) -> bool {
|
||||
@@ -99,10 +106,57 @@ impl StorageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn canonicalize_storage_path(path: &Path) -> Result<PathBuf, String> {
|
||||
if crate::path_has_symlink_component(path) {
|
||||
return Err(format!(
|
||||
"storage path contains a symlinked component: '{}'",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
let mut existing = path;
|
||||
let mut missing = Vec::new();
|
||||
loop {
|
||||
match std::fs::symlink_metadata(existing) {
|
||||
Ok(metadata) => {
|
||||
if metadata.file_type().is_symlink() {
|
||||
return Err(format!(
|
||||
"storage path contains a symlinked directory: '{}'",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"failed to inspect storage path '{}': {error}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
missing.push(
|
||||
existing
|
||||
.file_name()
|
||||
.ok_or_else(|| format!("storage path has no existing ancestor: '{}'", path.display()))?
|
||||
.to_owned(),
|
||||
);
|
||||
existing = existing
|
||||
.parent()
|
||||
.ok_or_else(|| format!("storage path has no existing ancestor: '{}'", path.display()))?;
|
||||
}
|
||||
let mut canonical = std::fs::canonicalize(existing)
|
||||
.map_err(|error| format!("failed to canonicalize storage path '{}': {error}", path.display()))?;
|
||||
for component in missing.iter().rev() {
|
||||
canonical.push(component);
|
||||
}
|
||||
Ok(canonical)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{StorageMode, PORTABLE_MARKER};
|
||||
use super::{canonicalize_storage_path, StorageMode, PORTABLE_MARKER};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
@@ -127,4 +181,31 @@ mod tests {
|
||||
StorageMode::Standard
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn rejects_symlinked_storage_directories() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let root = TempDir::new().unwrap();
|
||||
let target = TempDir::new().unwrap();
|
||||
let root_path = fs::canonicalize(root.path()).unwrap();
|
||||
let redirected = root_path.join("logs");
|
||||
symlink(target.path(), &redirected).unwrap();
|
||||
|
||||
assert!(canonicalize_storage_path(Path::new(&redirected)).is_err());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn rejects_dangling_symlinked_storage_directories() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let root = TempDir::new().unwrap();
|
||||
let root_path = fs::canonicalize(root.path()).unwrap();
|
||||
let redirected = root_path.join("logs");
|
||||
symlink(root_path.join("missing-target"), &redirected).unwrap();
|
||||
|
||||
assert!(canonicalize_storage_path(Path::new(&redirected)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user