From 42568b6494b902dacf766a12eba83eb33e768601 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:03:57 -0500 Subject: [PATCH] [PM-26316] Prevent users from sharing archived cipher (#6443) * prevent users from sharing an archived cipher * move check outside of encrypted check * add check for cipher stored in the DB does not have an archive date --- .../Vault/Controllers/CiphersController.cs | 15 +++ .../Controllers/CiphersControllerTests.cs | 112 ++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 06c88ad9bb..eb04ac1210 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -754,6 +754,11 @@ public class CiphersController : Controller } } + if (cipher.ArchivedDate.HasValue) + { + throw new BadRequestException("Cannot move an archived item to an organization."); + } + ValidateClientVersionForFido2CredentialSupport(cipher); var original = cipher.Clone(); @@ -1263,6 +1268,11 @@ public class CiphersController : Controller _logger.LogError("Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", cipher.Id, userId, cipher.EncryptedFor); throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } + + if (cipher.ArchivedDate.HasValue) + { + throw new BadRequestException("Cannot move archived items to an organization."); + } } var shareCiphers = new List<(CipherDetails, DateTime?)>(); @@ -1275,6 +1285,11 @@ public class CiphersController : Controller ValidateClientVersionForFido2CredentialSupport(existingCipher); + if (existingCipher.ArchivedDate.HasValue) + { + throw new BadRequestException("Cannot move archived items to an organization."); + } + shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate)); } diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 416b92f841..9f54cdbea5 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -1790,6 +1790,118 @@ public class CiphersControllerTests ); } + [Theory, BitAutoData] + public async Task PutShareMany_ArchivedCipher_ThrowsBadRequestException( + Guid organizationId, + Guid userId, + CipherWithIdRequestModel request, + SutProvider sutProvider) + { + request.EncryptedFor = userId; + request.OrganizationId = organizationId.ToString(); + request.ArchivedDate = DateTime.UtcNow; + var model = new CipherBulkShareRequestModel + { + Ciphers = [request], + CollectionIds = [Guid.NewGuid().ToString()] + }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetProperUserId(default) + .ReturnsForAnyArgs(userId); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShareMany(model) + ); + + Assert.Equal("Cannot move archived items to an organization.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PutShareMany_ExistingCipherArchived_ThrowsBadRequestException( + Guid organizationId, + Guid userId, + CipherWithIdRequestModel request, + SutProvider sutProvider) + { + // Request model does not have ArchivedDate (only the existing cipher does) + request.EncryptedFor = userId; + request.OrganizationId = organizationId.ToString(); + request.ArchivedDate = null; + + var model = new CipherBulkShareRequestModel + { + Ciphers = [request], + CollectionIds = [Guid.NewGuid().ToString()] + }; + + // The existing cipher from the repository IS archived + var existingCipher = new CipherDetails + { + Id = request.Id!.Value, + UserId = userId, + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()), + ArchivedDate = DateTime.UtcNow + }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetProperUserId(default) + .ReturnsForAnyArgs(userId); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId, withOrganizations: false) + .Returns(Task.FromResult((ICollection)[existingCipher])); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShareMany(model) + ); + + Assert.Equal("Cannot move archived items to an organization.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PutShare_ArchivedCipher_ThrowsBadRequestException( + Guid cipherId, + Guid organizationId, + User user, + CipherShareRequestModel model, + SutProvider sutProvider) + { + model.Cipher.OrganizationId = organizationId.ToString(); + model.Cipher.EncryptedFor = user.Id; + + var cipher = new Cipher + { + Id = cipherId, + UserId = user.Id, + ArchivedDate = DateTime.UtcNow.AddDays(-1), + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()) + }; + + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); + sutProvider.GetDependency() + .GetByIdAsync(cipherId) + .Returns(cipher); + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShare(cipherId, model) + ); + + Assert.Equal("Cannot move an archived item to an organization.", exception.Message); + } + [Theory, BitAutoData] public async Task PostPurge_WhenUserNotFound_ThrowsUnauthorizedAccessException( SecretVerificationRequestModel model,