Fix: cifs-Credentials-Datei lehnt Newlines und leeres Passwort ab

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjyzpGWECyKSBWz5DtkTGz
This commit is contained in:
2026-09-15 23:06:44 +02:00
co-authored by Claude Sonnet 5
parent 690f727600
commit fa1592e18e
+53
View File
@@ -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);