High severity8.0NVD Advisory· Published May 11, 2026· Updated May 16, 2026
CVE-2026-43639
CVE-2026-43639
Description
Bitwarden Server prior to v2026.4.0 contains a missing authorization vulnerability that allows a provider service user to add an arbitrary organization to their provider via POST /providers/{providerId}/clients/existing, resulting in takeover of the target organization; self-hosted installations are unaffected as this endpoint is restricted to Cloud via SelfHosted(NotSelfHostedOnly = true).
Affected products
1Patches
10918bfdda6f5Add checks and tests to provider controllers (#7372)
7 files changed · +559 −7
src/Api/AdminConsole/Controllers/ProviderClientsController.cs+15 −3 modified@@ -171,18 +171,30 @@ public async Task<IResult> AddExistingOrganizationAsync( [FromRoute] Guid providerId, [FromBody] AddExistingOrganizationRequestBody requestBody) { - var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId); + var userId = _currentContext.UserId; + if (!userId.HasValue) + { + return Error.Unauthorized(); + } + + var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); if (provider == null) { return result; } - var organization = await organizationRepository.GetByIdAsync(requestBody.OrganizationId); + if (!await _currentContext.OrganizationOwner(requestBody.OrganizationId)) + { + return Error.Unauthorized(); + } + + var addableOrganizations = await organizationRepository.GetAddableToProviderByUserIdAsync(userId.Value, provider.Type); + var organization = addableOrganizations.FirstOrDefault(o => o.Id == requestBody.OrganizationId); if (organization == null) { - return Error.BadRequest("The organization being added to the provider does not exist."); + return Error.NotFound(); } await providerBillingService.AddExistingOrganization(provider, organization, requestBody.Key);
src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs+2 −1 modified@@ -63,7 +63,8 @@ public async Task<ListResponseModel<ProviderOrganizationOrganizationDetailsRespo [HttpPost("add")] public async Task Add(Guid providerId, [FromBody] ProviderOrganizationAddRequestModel model) { - if (!_currentContext.ManageProviderOrganizations(providerId)) + if (!_currentContext.ManageProviderOrganizations(providerId) || + !await _currentContext.OrganizationOwner(model.OrganizationId)) { throw new NotFoundException(); }
src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs+2 −3 modified@@ -11,7 +11,6 @@ using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; -using LinqToDB.Tools; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -177,7 +176,7 @@ public async Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync() var query = from o in dbContext.Organizations - where o.PlanType.NotIn(disallowedPlanTypes) && + where !disallowedPlanTypes.Contains(o.PlanType) && !dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) && (string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%")) select o; @@ -400,7 +399,7 @@ join organization in dbContext.Organizations on organizationUser.OrganizationId organization.Seats > 0 && organization.Status == OrganizationStatusType.Created && !organization.UseSecretsManager && - organization.PlanType.In(planTypes) + planTypes.Contains(organization.PlanType) select organization; return await query.ToArrayAsync();
test/Api.IntegrationTest/AdminConsole/Controllers/ProviderClientsControllerTests.cs+230 −0 added@@ -0,0 +1,230 @@ +using System.Net; +using Bit.Api.Billing.Models.Requests; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Providers.Services; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using NSubstitute; +using Xunit; + +namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; + +public class ProviderClientsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + + private string _providerAdminEmail = null!; + private string _serviceUserEmail = null!; + private string _otherUserEmail = null!; + private Provider _provider = null!; + private Organization _addableOrg = null!; + private Organization _managedOrg = null!; + private Organization _otherOrg = null!; + + public ProviderClientsControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _factory.SubstituteService<IProviderBillingService>(_ => { }); + _client = _factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + } + + public async Task InitializeAsync() + { + _providerAdminEmail = $"{Guid.NewGuid()}@test.com"; + await _factory.LoginWithNewAccount(_providerAdminEmail); + + _serviceUserEmail = $"{Guid.NewGuid()}@test.com"; + await _factory.LoginWithNewAccount(_serviceUserEmail); + + _otherUserEmail = $"{Guid.NewGuid()}@test.com"; + await _factory.LoginWithNewAccount(_otherUserEmail); + + var userRepository = _factory.GetService<IUserRepository>(); + var orgRepository = _factory.GetService<IOrganizationRepository>(); + var orgUserRepository = _factory.GetService<IOrganizationUserRepository>(); + var providerRepository = _factory.GetService<IProviderRepository>(); + var providerUserRepository = _factory.GetService<IProviderUserRepository>(); + + var providerAdmin = await userRepository.GetByEmailAsync(_providerAdminEmail); + var serviceUser = await userRepository.GetByEmailAsync(_serviceUserEmail); + var otherUser = await userRepository.GetByEmailAsync(_otherUserEmail); + + _provider = await providerRepository.CreateAsync(new Provider + { + Name = "Test MSP Provider", + BillingEmail = "billing@test.com", + Type = ProviderType.Msp, + Status = ProviderStatusType.Billable, + Enabled = true, + GatewayCustomerId = $"cus_{Guid.NewGuid():N}", + GatewaySubscriptionId = $"sub_{Guid.NewGuid():N}" + }); + + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = _provider.Id, + UserId = providerAdmin!.Id, + Type = ProviderUserType.ProviderAdmin, + Status = ProviderUserStatusType.Confirmed, + Key = Guid.NewGuid().ToString() + }); + + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = _provider.Id, + UserId = serviceUser!.Id, + Type = ProviderUserType.ServiceUser, + Status = ProviderUserStatusType.Confirmed, + Key = Guid.NewGuid().ToString() + }); + + _addableOrg = await orgRepository.CreateAsync(new Organization + { + Name = "Addable Org", + BillingEmail = _providerAdminEmail, + Plan = "Teams (Monthly)", + PlanType = PlanType.TeamsMonthly, + Status = OrganizationStatusType.Created, + Enabled = true, + Seats = 10, + GatewayCustomerId = $"cus_{Guid.NewGuid():N}", + GatewaySubscriptionId = $"sub_{Guid.NewGuid():N}", + UseSecretsManager = false + }); + + await orgUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = _addableOrg.Id, + UserId = providerAdmin.Id, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Confirmed, + AccessSecretsManager = false + }); + + _managedOrg = await orgRepository.CreateAsync(new Organization + { + Name = "Managed Org", + BillingEmail = _providerAdminEmail, + Plan = "Teams (Monthly)", + PlanType = PlanType.TeamsMonthly, + Status = OrganizationStatusType.Managed, + Enabled = true, + Seats = 10, + GatewayCustomerId = $"cus_{Guid.NewGuid():N}", + GatewaySubscriptionId = $"sub_{Guid.NewGuid():N}", + UseSecretsManager = false + }); + + await orgUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = _managedOrg.Id, + UserId = providerAdmin.Id, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Confirmed, + AccessSecretsManager = false + }); + + _otherOrg = await orgRepository.CreateAsync(new Organization + { + Name = "Other User Org", + BillingEmail = _otherUserEmail, + Plan = "Teams (Monthly)", + PlanType = PlanType.TeamsMonthly, + Status = OrganizationStatusType.Created, + Enabled = true, + Seats = 10, + GatewayCustomerId = $"cus_{Guid.NewGuid():N}", + GatewaySubscriptionId = $"sub_{Guid.NewGuid():N}", + UseSecretsManager = false + }); + + await orgUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = _otherOrg.Id, + UserId = otherUser!.Id, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Confirmed, + AccessSecretsManager = false + }); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task AddExistingOrganizationAsync_Unauthenticated_ReturnsUnauthorized() + { + var request = new AddExistingOrganizationRequestBody { OrganizationId = _addableOrg.Id, Key = "key" }; + + var response = await _client.PostAsJsonAsync($"providers/{_provider.Id}/clients/existing", request); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task AddExistingOrganizationAsync_ServiceUser_ReturnsUnauthorized() + { + await _loginHelper.LoginAsync(_serviceUserEmail); + + var request = new AddExistingOrganizationRequestBody { OrganizationId = _addableOrg.Id, Key = "key" }; + + var response = await _client.PostAsJsonAsync($"providers/{_provider.Id}/clients/existing", request); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task AddExistingOrganizationAsync_NotOrgOwner_ReturnsUnauthorized() + { + await _loginHelper.LoginAsync(_providerAdminEmail); + + var request = new AddExistingOrganizationRequestBody { OrganizationId = _otherOrg.Id, Key = "key" }; + + var response = await _client.PostAsJsonAsync($"providers/{_provider.Id}/clients/existing", request); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task AddExistingOrganizationAsync_OrgNotAddable_ReturnsNotFound() + { + await _loginHelper.LoginAsync(_providerAdminEmail); + + var request = new AddExistingOrganizationRequestBody { OrganizationId = _managedOrg.Id, Key = "key" }; + + var response = await _client.PostAsJsonAsync($"providers/{_provider.Id}/clients/existing", request); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task AddExistingOrganizationAsync_ValidRequest_ReturnsOk() + { + await _loginHelper.LoginAsync(_providerAdminEmail); + + var request = new AddExistingOrganizationRequestBody { OrganizationId = _addableOrg.Id, Key = "key" }; + + var response = await _client.PostAsJsonAsync($"providers/{_provider.Id}/clients/existing", request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var billingService = _factory.GetService<IProviderBillingService>(); + await billingService.Received(1).AddExistingOrganization( + Arg.Is<Provider>(p => p.Id == _provider.Id), + Arg.Is<Organization>(o => o.Id == _addableOrg.Id), + "key"); + } +}
test/Api.IntegrationTest/AdminConsole/Controllers/ProviderOrganizationsControllerTests.cs+158 −0 added@@ -0,0 +1,158 @@ +using System.Net; +using Bit.Api.AdminConsole.Models.Request.Providers; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; + +public class ProviderOrganizationsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + + private string _providerAdminEmail = null!; + private string _otherUserEmail = null!; + private Provider _provider = null!; + private Organization _org = null!; + private Organization _otherOrg = null!; + + public ProviderOrganizationsControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _client = _factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + } + + public async Task InitializeAsync() + { + _providerAdminEmail = $"{Guid.NewGuid()}@test.com"; + await _factory.LoginWithNewAccount(_providerAdminEmail); + + _otherUserEmail = $"{Guid.NewGuid()}@test.com"; + await _factory.LoginWithNewAccount(_otherUserEmail); + + var userRepository = _factory.GetService<IUserRepository>(); + var orgRepository = _factory.GetService<IOrganizationRepository>(); + var orgUserRepository = _factory.GetService<IOrganizationUserRepository>(); + var providerRepository = _factory.GetService<IProviderRepository>(); + var providerUserRepository = _factory.GetService<IProviderUserRepository>(); + + var providerAdmin = await userRepository.GetByEmailAsync(_providerAdminEmail); + var otherUser = await userRepository.GetByEmailAsync(_otherUserEmail); + + _provider = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider", + BillingEmail = "billing@test.com", + Type = ProviderType.Msp, + Status = ProviderStatusType.Created, + Enabled = true + }); + + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = _provider.Id, + UserId = providerAdmin!.Id, + Type = ProviderUserType.ProviderAdmin, + Status = ProviderUserStatusType.Confirmed, + Key = Guid.NewGuid().ToString() + }); + + _org = await orgRepository.CreateAsync(new Organization + { + Name = "Provider Admin Org", + BillingEmail = _providerAdminEmail, + Plan = "Teams (Monthly)", + PlanType = PlanType.TeamsMonthly, + Status = OrganizationStatusType.Created, + Enabled = true, + Seats = 10, + }); + + await orgUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = _org.Id, + UserId = providerAdmin.Id, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Confirmed, + AccessSecretsManager = false + }); + + _otherOrg = await orgRepository.CreateAsync(new Organization + { + Name = "Other User Org", + BillingEmail = _otherUserEmail, + Plan = "Teams (Monthly)", + PlanType = PlanType.TeamsMonthly, + Status = OrganizationStatusType.Created, + Enabled = true, + Seats = 10, + UseSecretsManager = false + }); + + await orgUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = _otherOrg.Id, + UserId = otherUser!.Id, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Confirmed, + AccessSecretsManager = false + }); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task Add_Unauthenticated_ReturnsUnauthorized() + { + var model = new ProviderOrganizationAddRequestModel { OrganizationId = _org.Id, Key = "key" }; + + var response = await _client.PostAsJsonAsync($"providers/{_provider.Id}/organizations/add", model); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task Add_NotOrgOwner_ReturnsNotFound() + { + await _loginHelper.LoginAsync(_providerAdminEmail); + + var model = new ProviderOrganizationAddRequestModel { OrganizationId = _otherOrg.Id, Key = "key" }; + + var response = await _client.PostAsJsonAsync($"providers/{_provider.Id}/organizations/add", model); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Add_ValidRequest_ReturnsOk() + { + await _loginHelper.LoginAsync(_providerAdminEmail); + + var providerOrganizationRepository = _factory.GetService<IProviderOrganizationRepository>(); + + var model = new ProviderOrganizationAddRequestModel { OrganizationId = _org.Id, Key = "key" }; + + var response = await _client.PostAsJsonAsync($"providers/{_provider.Id}/organizations/add", model); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var providerOrganization = await providerOrganizationRepository.GetByOrganizationId(_org.Id); + Assert.NotNull(providerOrganization); + Assert.Equal(_provider.Id, providerOrganization.ProviderId); + } +}
test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs+85 −0 modified@@ -92,6 +92,91 @@ await sutProvider.GetDependency<IProviderBillingService>().Received(1).CreateCus #endregion + #region AddExistingOrganizationAsync + + [Theory, BitAutoData] + public async Task AddExistingOrganizationAsync_ServiceUser_Unauthorized( + Provider provider, + AddExistingOrganizationRequestBody requestBody, + SutProvider<ProviderClientsController> sutProvider) + { + ConfigureStableProviderServiceUserInputs(provider, sutProvider); + + var result = await sutProvider.Sut.AddExistingOrganizationAsync(provider.Id, requestBody); + + AssertUnauthorized(result); + } + + [Theory, BitAutoData] + public async Task AddExistingOrganizationAsync_NotOrgOwner_Unauthorized( + Provider provider, + AddExistingOrganizationRequestBody requestBody, + SutProvider<ProviderClientsController> sutProvider) + { + ConfigureStableProviderAdminInputs(provider, sutProvider); + + sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(requestBody.OrganizationId) + .Returns(false); + + var result = await sutProvider.Sut.AddExistingOrganizationAsync(provider.Id, requestBody); + + AssertUnauthorized(result); + } + + [Theory, BitAutoData] + public async Task AddExistingOrganizationAsync_OrgNotAddable_NotFound( + Provider provider, + AddExistingOrganizationRequestBody requestBody, + Guid userId, + SutProvider<ProviderClientsController> sutProvider) + { + ConfigureStableProviderAdminInputs(provider, sutProvider); + + sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(requestBody.OrganizationId) + .Returns(true); + + sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); + + sutProvider.GetDependency<IOrganizationRepository>() + .GetAddableToProviderByUserIdAsync(userId, provider.Type) + .Returns([]); + + var result = await sutProvider.Sut.AddExistingOrganizationAsync(provider.Id, requestBody); + + AssertNotFound(result); + } + + [Theory, BitAutoData] + public async Task AddExistingOrganizationAsync_Ok( + Provider provider, + AddExistingOrganizationRequestBody requestBody, + Organization organization, + Guid userId, + SutProvider<ProviderClientsController> sutProvider) + { + organization.Id = requestBody.OrganizationId; + + ConfigureStableProviderAdminInputs(provider, sutProvider); + + sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(requestBody.OrganizationId) + .Returns(true); + + sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); + + sutProvider.GetDependency<IOrganizationRepository>() + .GetAddableToProviderByUserIdAsync(userId, provider.Type) + .Returns([organization]); + + var result = await sutProvider.Sut.AddExistingOrganizationAsync(provider.Id, requestBody); + + await sutProvider.GetDependency<IProviderBillingService>().Received(1) + .AddExistingOrganization(provider, organization, requestBody.Key); + + Assert.IsType<Ok>(result); + } + + #endregion + #region UpdateAsync [Theory, BitAutoData]
test/Api.Test/AdminConsole/Controllers/ProviderOrganizationsControllerTests.cs+67 −0 added@@ -0,0 +1,67 @@ +using Bit.Api.AdminConsole.Controllers; +using Bit.Api.AdminConsole.Models.Request.Providers; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Controllers; + +[ControllerCustomize(typeof(ProviderOrganizationsController))] +[SutProviderCustomize] +public class ProviderOrganizationsControllerTests +{ + [Theory, BitAutoData] + public async Task Add_NotProviderAdmin_ThrowsNotFound( + Guid providerId, + ProviderOrganizationAddRequestModel model, + SutProvider<ProviderOrganizationsController> sutProvider) + { + sutProvider.GetDependency<ICurrentContext>().ManageProviderOrganizations(providerId) + .Returns(false); + + await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Add(providerId, model)); + + await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs() + .AddOrganization(default, default, default); + } + + [Theory, BitAutoData] + public async Task Add_NotOrgOwner_ThrowsNotFound( + Guid providerId, + ProviderOrganizationAddRequestModel model, + SutProvider<ProviderOrganizationsController> sutProvider) + { + sutProvider.GetDependency<ICurrentContext>().ManageProviderOrganizations(providerId) + .Returns(true); + + sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId) + .Returns(false); + + await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Add(providerId, model)); + + await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs() + .AddOrganization(default, default, default); + } + + [Theory, BitAutoData] + public async Task Add_Ok( + Guid providerId, + ProviderOrganizationAddRequestModel model, + SutProvider<ProviderOrganizationsController> sutProvider) + { + sutProvider.GetDependency<ICurrentContext>().ManageProviderOrganizations(providerId) + .Returns(true); + + sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId) + .Returns(true); + + await sutProvider.Sut.Add(providerId, model); + + await sutProvider.GetDependency<IProviderService>().Received(1) + .AddOrganization(providerId, model.OrganizationId, model.Key); + } +}
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
5- github.com/bitwarden/server/commit/0918bfdda6f5eec391c69bd9074f6aef4eac0b1dnvdPatch
- github.com/bitwarden/server/pull/7372nvdIssue TrackingPatch
- sanjokkarki.com.np/blog/bitwarden-provider-takeovernvdExploitThird Party Advisory
- www.vulncheck.com/advisories/bitwarden-server-missing-authorization-via-provider-clientsnvdThird Party Advisory
- github.com/bitwarden/server/releases/tag/v2026.4.0nvdRelease Notes
News mentions
0No linked articles in our index yet.