From fa1592e18e20c4231ffb07afe529374af5e0a5fb Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Tue, 15 Sep 2026 23:06:44 +0200 Subject: [PATCH] Fix: cifs-Credentials-Datei lehnt Newlines und leeres Passwort ab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit write_credentials_file() schrieb username/password/domain ungeprüft in die zeilenbasierte (key=value) Credentials-Datei für mount.cifs - ein eingebettetes Newline in einem Feld hätte eine zusätzliche, nicht vorgesehene Zeile einschleusen können (z. B. ein überraschendes domain= oder ein zweites password=, das das eigentliche überschreibt). Felder mit Newline/Carriage-Return sowie ein leeres Passwort werden jetzt vor dem Schreiben abgelehnt. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LjyzpGWECyKSBWz5DtkTGz --- src/mount/smb.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/mount/smb.rs b/src/mount/smb.rs index 2553039..29789e9 100644 --- a/src/mount/smb.rs +++ b/src/mount/smb.rs @@ -75,7 +75,33 @@ pub fn credentials_path(pair_id: &str, side: crate::db::credentials::Side) -> Pa base.join(format!("{pair_id}-{}.cred", side.as_str())) } +/// `mount.cifs`s Credentials-Datei ist zeilenbasiert (`key=value`) - ein eingebettetes +/// Newline in einem Feld würde eine zusätzliche, nicht vorgesehene `key=value`-Zeile +/// einschleusen (z. B. ein überraschendes `domain=...` oder ein zweites `password=`, das das +/// eigentliche überschreibt). Ein leeres Passwort ist ebenfalls kein sinnvoller Wert. +fn validate_credential_field(name: &str, value: &str) -> Result<()> { + if value.contains(['\n', '\r']) { + return Err(Error::Other(format!( + "cifs credential field '{name}' must not contain a newline" + ))); + } + Ok(()) +} + fn write_credentials_file(path: &PathBuf, cred: &Credential) -> Result<()> { + if let Some(username) = &cred.username { + validate_credential_field("username", username)?; + } + validate_credential_field("password", &cred.password)?; + if cred.password.is_empty() { + return Err(Error::Other( + "cifs credential has an empty password".to_string(), + )); + } + if let Some(domain) = &cred.domain { + validate_credential_field("domain", domain)?; + } + if let Some(dir) = path.parent() { std::fs::create_dir_all(dir).map_err(|e| Error::io(dir, e))?; } @@ -138,6 +164,33 @@ mod tests { assert!(contents.contains("domain=WORKGROUP")); } + #[test] + fn rejects_password_with_embedded_newline() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("test.cred"); + let cred = Credential { + username: Some("nasuser".to_string()), + domain: None, + password: "s3cret\ndomain=INJECTED".to_string(), + }; + + assert!(write_credentials_file(&path, &cred).is_err()); + assert!(!path.exists()); + } + + #[test] + fn rejects_empty_password() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("test.cred"); + let cred = Credential { + username: Some("nasuser".to_string()), + domain: None, + password: String::new(), + }; + + assert!(write_credentials_file(&path, &cred).is_err()); + } + #[test] fn credentials_path_differs_per_side() { let local = credentials_path("pair-1", Side::Local);