From ffc567d9e969f33be092f5768f5a82c21073bad7 Mon Sep 17 00:00:00 2001 From: xhon-pelushi Date: Wed, 12 Aug 2026 00:39:18 -0400 Subject: [PATCH] Reject SSH key ciphers with null or empty required fields Validate privateKey, publicKey, and keyFingerprint before saving type-5 ciphers. Bitwarden cloud rejects these payloads; accepting them caused a successful write followed by silent sshKey loss on read-back. Fixes #7514 --- src/api/core/ciphers.rs | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 2b51fd0c..cf5c493e 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -377,6 +377,17 @@ async fn post_ciphers(data: Json, headers: Headers, conn: DbConn, nt /// Enforces the personal ownership policy on user-owned ciphers, if applicable. /// A non-owner/admin user belonging to an org with the personal ownership policy /// enabled isn't allowed to create new user-owned ciphers or modify existing ones +/// Ensure SSH key type-data has the required non-empty string fields. +fn validate_ssh_key_data(type_data: &Value) -> EmptyResult { + for field in ["privateKey", "publicKey", "keyFingerprint"] { + match type_data.get(field).and_then(Value::as_str) { + Some(value) if !value.is_empty() => {} + _ => err!(format!("SSH key field '{field}' must be a non-empty string")), + } + } + Ok(()) +} + /// (that were created before the policy was applicable to the user). The user is /// allowed to delete or share such ciphers to an org, however. /// @@ -525,6 +536,13 @@ pub async fn update_cipher_from_data( err!("Data missing") }; + // Reject invalid SSH key payloads up-front. Bitwarden cloud returns a validation + // error for null/empty required members; previously we accepted the write and then + // dropped sshKey on read-back (silent data loss). + if data.r#type == 5 { + validate_ssh_key_data(&type_data)?; + } + cipher.key = data.key; cipher.name = data.name; cipher.notes = data.notes; @@ -2221,3 +2239,48 @@ impl CipherSyncData { } } } + +#[cfg(test)] +mod ssh_key_validation_tests { + use super::validate_ssh_key_data; + use serde_json::json; + + #[test] + fn accepts_non_empty_required_fields() { + let data = json!({ + "privateKey": "priv", + "publicKey": "pub", + "keyFingerprint": "fp" + }); + assert!(validate_ssh_key_data(&data).is_ok()); + } + + #[test] + fn rejects_null_private_key() { + let data = json!({ + "privateKey": null, + "publicKey": "pub", + "keyFingerprint": "fp" + }); + assert!(validate_ssh_key_data(&data).is_err()); + } + + #[test] + fn rejects_empty_public_key() { + let data = json!({ + "privateKey": "priv", + "publicKey": "", + "keyFingerprint": "fp" + }); + assert!(validate_ssh_key_data(&data).is_err()); + } + + #[test] + fn rejects_missing_fingerprint() { + let data = json!({ + "privateKey": "priv", + "publicKey": "pub" + }); + assert!(validate_ssh_key_data(&data).is_err()); + } +}