From 8869652dad7ec551fa8a168f2d95cded0958994b Mon Sep 17 00:00:00 2001 From: easonysliu Date: Thu, 19 Mar 2026 01:12:31 +0800 Subject: [PATCH] Fix "Invalid folder" error when client sends empty folderId string Newer Bitwarden client versions (e.g. browser extension v2026.2.0) send folderId as "" (empty string) instead of null when "No folder" is selected. This causes the folder validation to attempt a database lookup with an empty string, which fails with "Invalid folder" and prevents saving the cipher. Normalize empty folderId to None at all three validation points: - update_cipher_from_data (create/update cipher) - put_cipher_partial (update cipher details) - move_cipher_selected (move ciphers to folder) Fixes #6962 --- src/api/core/ciphers.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index f7bf5cd3..98623aa9 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -448,7 +448,11 @@ pub async fn update_cipher_from_data( cipher.user_uuid = Some(headers.user.uuid.clone()); } - if let Some(ref folder_id) = data.folder_id { + // Newer Bitwarden clients may send folderId as "" instead of null for "No folder". + // Normalize empty folder_id to None to avoid invalid folder lookup errors. + let folder_id = data.folder_id.filter(|f| !f.is_empty()); + + if let Some(ref folder_id) = folder_id { if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, conn).await.is_none() { err!("Invalid folder", "Folder does not exist or belongs to another user"); } @@ -528,7 +532,7 @@ pub async fn update_cipher_from_data( cipher.reprompt = data.reprompt.filter(|r| *r == RepromptType::None as i32 || *r == RepromptType::Password as i32); cipher.save(conn).await?; - cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?; + cipher.move_to_folder(folder_id, &headers.user.uuid, conn).await?; cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?; if ut != UpdateType::None { @@ -722,14 +726,16 @@ async fn put_cipher_partial( err!("Cipher does not exist", "Cipher is not accessible for the current user") } - if let Some(ref folder_id) = data.folder_id { + let folder_id = data.folder_id.filter(|f| !f.is_empty()); + + if let Some(ref folder_id) = folder_id { if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none() { err!("Invalid folder", "Folder does not exist or belongs to another user"); } } // Move cipher - cipher.move_to_folder(data.folder_id.clone(), &headers.user.uuid, &conn).await?; + cipher.move_to_folder(folder_id, &headers.user.uuid, &conn).await?; // Update favorite cipher.set_favorite(Some(data.favorite), &headers.user.uuid, &conn).await?; @@ -1582,7 +1588,9 @@ async fn move_cipher_selected( let data = data.into_inner(); let user_id = &headers.user.uuid; - if let Some(ref folder_id) = data.folder_id { + let folder_id = data.folder_id.filter(|f| !f.is_empty()); + + if let Some(ref folder_id) = folder_id { if Folder::find_by_uuid_and_user(folder_id, user_id, &conn).await.is_none() { err!("Invalid folder", "Folder does not exist or belongs to another user"); } @@ -1596,7 +1604,7 @@ async fn move_cipher_selected( let accessible_ciphers = Cipher::find_by_user_and_ciphers(user_id, &data.ids, &conn).await; let accessible_ciphers_count = accessible_ciphers.len(); for cipher in accessible_ciphers { - cipher.move_to_folder(data.folder_id.clone(), user_id, &conn).await?; + cipher.move_to_folder(folder_id.clone(), user_id, &conn).await?; if cipher_count == 1 { single_cipher = Some(cipher); }