fix: prevent path traversal, enable CSP, and harden URL domain extraction
Validate that resolved list paths stay within the workspace root to prevent directory traversal via malicious list names. Enable Content Security Policy in Tauri config instead of leaving it null. Fix CLI domain extraction to strip userinfo (user:pass@) from URLs before using as keyring service name.
This commit is contained in:
parent
40142cb1ca
commit
68f1bff93b
|
|
@ -23,7 +23,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; connect-src ipc: http://ipc.localhost"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|
|
||||||
|
|
@ -204,18 +204,20 @@ fn print_workspace_status(name: &str, path: &std::path::Path, webdav_url: Option
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract domain from a URL for credential storage.
|
/// Extract host from a URL for credential storage.
|
||||||
fn extract_domain(url: &str) -> String {
|
fn extract_domain(url: &str) -> String {
|
||||||
url.split("://")
|
// Strip scheme
|
||||||
.nth(1)
|
let after_scheme = url.split("://").nth(1).unwrap_or(url);
|
||||||
.unwrap_or(url)
|
// Strip path
|
||||||
.split('/')
|
let authority = after_scheme.split('/').next().unwrap_or(after_scheme);
|
||||||
.next()
|
// Strip userinfo (user:pass@host)
|
||||||
.unwrap_or(url)
|
let host_port = if let Some(at_pos) = authority.rfind('@') {
|
||||||
.split(':')
|
&authority[at_pos + 1..]
|
||||||
.next()
|
} else {
|
||||||
.unwrap_or(url)
|
authority
|
||||||
.to_string()
|
};
|
||||||
|
// Strip port
|
||||||
|
host_port.split(':').next().unwrap_or(host_port).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prompt the user for text input.
|
/// Prompt the user for text input.
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,30 @@ impl FileSystemStorage {
|
||||||
Err(Error::ListNotFound(list_id.to_string()))
|
Err(Error::ListNotFound(list_id.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_dir_path_by_name(&self, name: &str) -> PathBuf {
|
fn list_dir_path_by_name(&self, name: &str) -> Result<PathBuf> {
|
||||||
self.root_path.join(name)
|
let path = self.root_path.join(name);
|
||||||
|
// Prevent path traversal: resolved path must stay within root
|
||||||
|
let canonical_root = self.root_path.canonicalize()
|
||||||
|
.unwrap_or_else(|_| self.root_path.clone());
|
||||||
|
let canonical_path = if path.exists() {
|
||||||
|
path.canonicalize().unwrap_or_else(|_| path.clone())
|
||||||
|
} else {
|
||||||
|
// For non-existent paths, normalize by resolving the parent
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let canonical_parent = if parent.exists() {
|
||||||
|
parent.canonicalize().unwrap_or_else(|_| parent.to_path_buf())
|
||||||
|
} else {
|
||||||
|
parent.to_path_buf()
|
||||||
|
};
|
||||||
|
canonical_parent.join(path.file_name().unwrap_or_default())
|
||||||
|
} else {
|
||||||
|
path.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !canonical_path.starts_with(&canonical_root) {
|
||||||
|
return Err(Error::InvalidData(format!("Invalid list name: path escapes workspace")));
|
||||||
|
}
|
||||||
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sanitize_filename(name: &str) -> String {
|
fn sanitize_filename(name: &str) -> String {
|
||||||
|
|
@ -371,7 +393,7 @@ impl Storage for FileSystemStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_list(&mut self, name: String) -> Result<TaskList> {
|
fn create_list(&mut self, name: String) -> Result<TaskList> {
|
||||||
let list_dir = self.list_dir_path_by_name(&name);
|
let list_dir = self.list_dir_path_by_name(&name)?;
|
||||||
|
|
||||||
if list_dir.exists() {
|
if list_dir.exists() {
|
||||||
return Err(Error::InvalidData(format!("List '{}' already exists", name)));
|
return Err(Error::InvalidData(format!("List '{}' already exists", name)));
|
||||||
|
|
@ -473,7 +495,7 @@ impl Storage for FileSystemStorage {
|
||||||
|
|
||||||
fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()> {
|
fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()> {
|
||||||
let old_dir = self.list_dir_path(list_id)?;
|
let old_dir = self.list_dir_path(list_id)?;
|
||||||
let new_dir = self.list_dir_path_by_name(&new_name);
|
let new_dir = self.list_dir_path_by_name(&new_name)?;
|
||||||
|
|
||||||
if new_dir.exists() {
|
if new_dir.exists() {
|
||||||
return Err(Error::InvalidData(format!("A list named '{}' already exists", new_name)));
|
return Err(Error::InvalidData(format!("A list named '{}' already exists", new_name)));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue