From 8e965902c60833d1d7f417792e0e56850c551a9c Mon Sep 17 00:00:00 2001 From: Yunare Maia Date: Mon, 24 Aug 2026 14:40:50 +0000 Subject: [PATCH] fix(api): reject SSH Key ciphers with missing or empty key members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A type-5 cipher whose sshKey object carries null (or non-string) values in privateKey, publicKey or keyFingerprint was accepted and stored, but clients drop the malformed payload on read: the save looks successful and the key material silently disappears. Bitwarden's own server rejects the same request with a validation error, so a client that is correct against cloud gets silent data loss against Vaultwarden. Require all three members to be present as non-empty strings before persisting a type-5 cipher, matching the cloud behavior described in #7514. This is a presence check only — key material itself is not parsed or validated. Fixes #7514 Signed-off-by: Yunare Maia --- src/api/core/ciphers.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 2b51fd0c..6191e26f 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -513,14 +513,28 @@ pub async fn update_cipher_from_data( _ => err!("Invalid type"), }; - let type_data = if let Some(mut data) = type_data_opt { + let type_data = if let Some(mut type_value) = type_data_opt { // Remove the 'Response' key from the base object. - data.as_object_mut().unwrap().remove("response"); + type_value.as_object_mut().unwrap().remove("response"); // Remove the 'Response' key from every Uri. - if data["uris"].is_array() { - data["uris"] = clean_cipher_data(data["uris"].clone()); + if type_value["uris"].is_array() { + type_value["uris"] = clean_cipher_data(type_value["uris"].clone()); } - data + // An SSH Key cipher (type 5) requires all three key members to be + // non-empty strings. Bitwarden's server rejects the request otherwise; + // accepting it here would store a cipher whose sshKey payload is + // dropped by clients on read, which looks like a successful save but + // silently loses the key material. + if data.r#type == 5 { + let obj = type_value.as_object().unwrap(); + for member in ["privateKey", "publicKey", "keyFingerprint"] { + let valid = matches!(obj.get(member), Some(Value::String(s)) if !s.is_empty()); + if !valid { + err!(format!("SshKey.{member} is required and must be a non-empty string")); + } + } + } + type_value } else { err!("Data missing") };