VYPR
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

1

Patches

1
ebbf6dd0fa75

[PM-34383] Add import validation allowing providers to perform imports (#7394)

https://github.com/bitwarden/serverJohn HarringtonApr 8, 2026via nvd-ref
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

News mentions

0

No linked articles in our index yet.