Medium severity5.4NVD Advisory· Published May 11, 2026· Updated May 16, 2026
CVE-2026-43638
CVE-2026-43638
Description
Bitwarden Server prior to v2026.4.1 contains a missing authorization vulnerability that allows any authenticated user to write ciphers into an arbitrary organization via POST /ciphers/import-organization by submitting an empty collections array, which causes the server-side permission check to be skipped.
Affected products
1Patches
1ebbf6dd0fa75[PM-34383] Add import validation allowing providers to perform imports (#7394)
4 files changed · +96 −29
src/Api/Tools/Controllers/ImportCiphersController.cs+2 −8 modified@@ -75,7 +75,7 @@ public async Task PostImportOrganization([FromQuery] string organizationId, var collections = model.Collections.Select(c => c.ToCollection(orgId)).ToList(); //An User is allowed to import if CanCreate Collections or has AccessToImportExport - var authorized = await CheckOrgImportPermission(collections, orgId); + var authorized = await CheckOrgImportPermissionAsync(collections, orgId); if (!authorized) { throw new BadRequestException("Not enough privileges to import into this organization."); @@ -86,7 +86,7 @@ public async Task PostImportOrganization([FromQuery] string organizationId, await _importCiphersCommand.ImportIntoOrganizationalVaultAsync(collections, ciphers, model.CollectionRelationships, userId); } - private async Task<bool> CheckOrgImportPermission(List<Collection> collections, Guid orgId) + private async Task<bool> CheckOrgImportPermissionAsync(List<Collection> collections, Guid orgId) { //Users are allowed to import if they have the AccessToImportExport permission if (await _currentContext.AccessImportExport(orgId)) @@ -101,12 +101,6 @@ private async Task<bool> CheckOrgImportPermission(List<Collection> collections, .Select(c => c.Id) .ToHashSet(); - // when there are no collections, then we can import - if (collections.Count == 0) - { - return true; - } - // are we trying to import into existing collections? var existingCollections = collections.Where(tc => orgCollectionIds.Contains(tc.Id));
src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs+32 −12 modified@@ -1,8 +1,6 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; @@ -23,6 +21,7 @@ public class ImportCiphersCommand : IImportCiphersCommand private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ICollectionRepository _collectionRepository; private readonly IPolicyRequirementQuery _policyRequirementQuery; + private readonly ICurrentContext _currentContext; public ImportCiphersCommand( ICipherRepository cipherRepository, @@ -31,7 +30,8 @@ public ImportCiphersCommand( IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IPushNotificationService pushService, - IPolicyRequirementQuery policyRequirementQuery) + IPolicyRequirementQuery policyRequirementQuery, + ICurrentContext currentContext) { _cipherRepository = cipherRepository; _folderRepository = folderRepository; @@ -40,6 +40,7 @@ public ImportCiphersCommand( _collectionRepository = collectionRepository; _pushService = pushService; _policyRequirementQuery = policyRequirementQuery; + _currentContext = currentContext; } public async Task ImportIntoIndividualVaultAsync( @@ -64,7 +65,7 @@ public async Task ImportIntoIndividualVaultAsync( if (cipher.UserId.HasValue && cipher.Favorite) { - cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":true}}"; + cipher.Favorites = $"{{\"{cipher.UserId.ToString()!.ToUpperInvariant()}\":true}}"; } if (cipher.UserId.HasValue && cipher.ArchivedDate.HasValue) @@ -78,7 +79,7 @@ public async Task ImportIntoIndividualVaultAsync( //Assign id to the ones that don't exist in DB //Need to keep the list order to create the relationships - List<Folder> newFolders = new List<Folder>(); + var newFolders = new List<Folder>(); foreach (var folder in folders) { if (!userfoldersIds.Contains(folder.Id)) @@ -99,7 +100,7 @@ public async Task ImportIntoIndividualVaultAsync( continue; } - cipher.Folders = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":" + + cipher.Folders = $"{{\"{cipher.UserId.ToString()!.ToUpperInvariant()}\":" + $"\"{folder.Id.ToString().ToUpperInvariant()}\"}}"; } @@ -116,12 +117,31 @@ public async Task ImportIntoOrganizationalVaultAsync( IEnumerable<KeyValuePair<int, int>> collectionRelationships, Guid importingUserId) { - var org = collections.Count > 0 ? - await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) : - await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value); + var orgId = collections.Count > 0 + ? collections[0].OrganizationId + : ciphers.FirstOrDefault(c => c.OrganizationId.HasValue)?.OrganizationId; + + if (orgId is null) + { + throw new BadRequestException("No organization ID found in the import data."); + } + + var org = await _organizationRepository.GetByIdAsync(orgId.Value); + if (org is null) + { + throw new NotFoundException("Organization not found."); + } + var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId); + // A managed service provider is expected to be able to perform imports on behalf of a managed org + // In this situation importingOrgUser will be null, cross-check MSP status + if (importingOrgUser is null && !await _currentContext.ProviderUserForOrgAsync(org.Id)) + { + throw new UnauthorizedAccessException( + "An organization import can only be performed by organization members or authorized providers"); + } - if (collections.Count > 0 && org != null && org.MaxCollections.HasValue) + if (collections.Count > 0 && org.MaxCollections.HasValue) { var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id); if (org.MaxCollections.Value < (collectionCount + collections.Count))
test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs+10 −9 modified@@ -738,7 +738,7 @@ await sutProvider.GetDependency<IImportCiphersCommand>() } [Theory, BitAutoData] - public async Task PostImportOrganization_ImportWithNoCollectionsWithCreatePermissionsOnlySuccessAsync( + public async Task PostImportOrganization_ImportWithNoCollectionsWithCreatePermissionsOnly_ThrowsBadRequestAsync( SutProvider<ImportCiphersController> sutProvider, IFixture fixture, User user) @@ -753,7 +753,7 @@ public async Task PostImportOrganization_ImportWithNoCollectionsWithCreatePermis SetupUserService(sutProvider, user); - // Import model includes new and existing collection + // Import model with no collections — previously bypassed all authorization var request = new ImportOrganizationCiphersRequestModel { Collections = new List<CollectionWithIdRequestModel>().ToArray(), // No collections @@ -790,15 +790,16 @@ public async Task PostImportOrganization_ImportWithNoCollectionsWithCreatePermis .GetManyByOrganizationIdAsync(orgId) .Returns(new List<Collection>()); - // Act - // import ciphers only and no collections - // User has Create permissions - // expected to be successful - await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request); + // Act & Assert + // With no collections and no AccessImportExport permission, + // the import should be rejected — empty collections must not bypass authorization + var exception = await Assert.ThrowsAsync<BadRequestException>(() => + sutProvider.Sut.PostImportOrganization(orgId.ToString(), request)); + + Assert.Equal("Not enough privileges to import into this organization.", exception.Message); - // Assert await sutProvider.GetDependency<IImportCiphersCommand>() - .Received(1) + .DidNotReceive() .ImportIntoOrganizationalVaultAsync( Arg.Any<List<Collection>>(), Arg.Any<List<CipherDetails>>(),
test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs+52 −0 modified@@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; @@ -246,6 +247,11 @@ public async Task ImportIntoOrganizationalVaultAsync_WithNullImportingOrgUser_Sk .GetByOrganizationAsync(organization.Id, importingUserId) .Returns((OrganizationUser)null); + // Importing user is a provider user for this organization + sutProvider.GetDependency<ICurrentContext>() + .ProviderUserForOrgAsync(organization.Id) + .Returns(true); + sutProvider.GetDependency<ICollectionRepository>() .GetManyByOrganizationIdAsync(organization.Id) .Returns(new List<Collection>()); @@ -262,6 +268,52 @@ await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync( await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId); } + [Theory, BitAutoData] + public async Task ImportIntoOrganizationalVaultAsync_WithNullImportingOrgUser_AndNotProvider_ThrowsUnauthorizedAccess( + Organization organization, + Guid importingUserId, + List<Collection> collections, + List<CipherDetails> ciphers, + SutProvider<ImportCiphersCommand> sutProvider) + { + organization.MaxCollections = null; + + foreach (var collection in collections) + { + collection.OrganizationId = organization.Id; + } + + foreach (var cipher in ciphers) + { + cipher.OrganizationId = organization.Id; + } + + KeyValuePair<int, int>[] collectionRelationships = { + new(0, 0), + new(1, 1), + new(2, 2) + }; + + sutProvider.GetDependency<IOrganizationRepository>() + .GetByIdAsync(organization.Id) + .Returns(organization); + + // Importing user is NOT an org member + sutProvider.GetDependency<IOrganizationUserRepository>() + .GetByOrganizationAsync(organization.Id, importingUserId) + .Returns((OrganizationUser)null); + + // Importing user is NOT a provider user for this organization + sutProvider.GetDependency<ICurrentContext>() + .ProviderUserForOrgAsync(organization.Id) + .Returns(false); + + var exception = await Assert.ThrowsAsync<UnauthorizedAccessException>(() => + sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId)); + + Assert.Contains("organization members or authorized providers", exception.Message); + } + [Theory, BitAutoData] public async Task ImportIntoIndividualVaultAsync_WithArchivedCiphers_PreservesArchiveStatus( Guid importingUserId,
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
5- github.com/bitwarden/server/commit/ebbf6dd0fa752114c09d73abb48ce32a50476758nvdPatch
- github.com/bitwarden/server/pull/7394nvdIssue TrackingPatch
- sanjokkarki.com.np/blog/bitwarden-import-org-bypassnvdExploitThird Party Advisory
- www.vulncheck.com/advisories/bitwarden-server-missing-authorization-via-organization-cipher-importnvdThird Party Advisory
- github.com/bitwarden/server/releases/tag/v2026.4.1nvdRelease Notes
News mentions
0No linked articles in our index yet.