security: additional credential hardening
- Use :: separator in scoped keyring keys to prevent ambiguity with usernames containing dots (e.g. com.onyx.webdav.host::user) - Auto-migrate legacy credentials to scoped format on load, removing old unscoped entries after successful migration - Add 10MB response size limit on PROPFIND to prevent memory exhaustion from malicious servers (checks Content-Length header + actual body) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c4df1413dd
commit
fa87dbe12b
|
|
@ -99,7 +99,16 @@ impl WebDavClient {
|
||||||
return Err(Error::WebDav(format!("PROPFIND failed with status {}", status)));
|
return Err(Error::WebDav(format!("PROPFIND failed with status {}", status)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = resp.text().await?;
|
// Reject oversized responses to prevent memory exhaustion from malicious servers
|
||||||
|
const MAX_PROPFIND_BYTES: u64 = 10 * 1024 * 1024;
|
||||||
|
if resp.content_length().unwrap_or(0) > MAX_PROPFIND_BYTES {
|
||||||
|
return Err(Error::WebDav("PROPFIND response too large (>10MB)".into()));
|
||||||
|
}
|
||||||
|
let bytes = resp.bytes().await?;
|
||||||
|
if bytes.len() as u64 > MAX_PROPFIND_BYTES {
|
||||||
|
return Err(Error::WebDav("PROPFIND response too large (>10MB)".into()));
|
||||||
|
}
|
||||||
|
let body = String::from_utf8_lossy(&bytes);
|
||||||
parse_propfind_response(&body, &self._base_url, path)
|
parse_propfind_response(&body, &self._base_url, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -401,7 +410,7 @@ fn extract_relative_path(href: &str, base_url: &str, request_path: &str) -> Stri
|
||||||
/// to prevent collisions when multiple accounts exist on the same server.
|
/// to prevent collisions when multiple accounts exist on the same server.
|
||||||
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> {
|
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> {
|
||||||
let service = format!("com.onyx.webdav.{}", domain);
|
let service = format!("com.onyx.webdav.{}", domain);
|
||||||
let scoped_service = format!("com.onyx.webdav.{}.{}", domain, username);
|
let scoped_service = format!("com.onyx.webdav.{}::{}", domain, username);
|
||||||
|
|
||||||
let user_entry = keyring::Entry::new(&service, "username")
|
let user_entry = keyring::Entry::new(&service, "username")
|
||||||
.map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?;
|
.map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?;
|
||||||
|
|
@ -437,18 +446,29 @@ pub fn load_credentials(domain: &str) -> Result<(Zeroizing<String>, Zeroizing<St
|
||||||
|
|
||||||
if let Ok(user) = user_entry.get_password() {
|
if let Ok(user) = user_entry.get_password() {
|
||||||
// Try scoped password key first (domain+username), fall back to legacy unscoped key
|
// Try scoped password key first (domain+username), fall back to legacy unscoped key
|
||||||
let scoped_service = format!("com.onyx.webdav.{}.{}", domain, user);
|
let scoped_service = format!("com.onyx.webdav.{}::{}", domain, user);
|
||||||
let pass = keyring::Entry::new(&scoped_service, "password")
|
let found = keyring::Entry::new(&scoped_service, "password")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|e| e.get_password().ok())
|
.and_then(|e| e.get_password().ok())
|
||||||
|
.map(|p| (p, false))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
// Migration fallback: try legacy unscoped password entry
|
// Migration fallback: try legacy unscoped password entry
|
||||||
keyring::Entry::new(&service, "password")
|
keyring::Entry::new(&service, "password")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|e| e.get_password().ok())
|
.and_then(|e| e.get_password().ok())
|
||||||
|
.map(|p| (p, true))
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(pass) = pass {
|
if let Some((pass, needs_migration)) = found {
|
||||||
|
// Auto-migrate legacy credentials to scoped format
|
||||||
|
if needs_migration {
|
||||||
|
if let Ok(entry) = keyring::Entry::new(&scoped_service, "password") {
|
||||||
|
let _ = entry.set_password(&pass);
|
||||||
|
}
|
||||||
|
if let Ok(legacy) = keyring::Entry::new(&service, "password") {
|
||||||
|
let _ = legacy.delete_credential();
|
||||||
|
}
|
||||||
|
}
|
||||||
return Ok((Zeroizing::new(user), Zeroizing::new(pass)));
|
return Ok((Zeroizing::new(user), Zeroizing::new(pass)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -496,7 +516,7 @@ pub fn delete_credentials(domain: &str) -> Result<()> {
|
||||||
.and_then(|e| e.get_password().ok());
|
.and_then(|e| e.get_password().ok());
|
||||||
|
|
||||||
if let Some(user) = &username {
|
if let Some(user) = &username {
|
||||||
let scoped_service = format!("com.onyx.webdav.{}.{}", domain, user);
|
let scoped_service = format!("com.onyx.webdav.{}::{}", domain, user);
|
||||||
if let Ok(entry) = keyring::Entry::new(&scoped_service, "password") {
|
if let Ok(entry) = keyring::Entry::new(&scoped_service, "password") {
|
||||||
let _ = entry.delete_credential();
|
let _ = entry.delete_credential();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue