mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 05:00:19 -05:00
[PM-17733] - [Privilege Escalation] - Unauthorised access allows limited access user to change password of Items (#5452)
* prevent view-only users from updating passwords * revert change to licensing service * add tests * check if organizationId is there * move logic to private method * move logic to private method * move logic into method * revert change to licensing service * throw exception when cipher key is created by hidden password users * fix tests * don't allow totp or passkeys changes from hidden password users * add tests * revert change to licensing service
This commit is contained in:
parent
fc827ed209
commit
21717ec71e
@ -13,7 +13,9 @@ using Bit.Core.Tools.Models.Business;
|
|||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
using Bit.Core.Vault.Queries;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.Vault.Services;
|
namespace Bit.Core.Vault.Services;
|
||||||
@ -38,6 +40,7 @@ public class CipherService : ICipherService
|
|||||||
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
|
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;
|
||||||
|
|
||||||
public CipherService(
|
public CipherService(
|
||||||
ICipherRepository cipherRepository,
|
ICipherRepository cipherRepository,
|
||||||
@ -54,7 +57,8 @@ public class CipherService : ICipherService
|
|||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
ICurrentContext currentContext)
|
ICurrentContext currentContext,
|
||||||
|
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery)
|
||||||
{
|
{
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@ -71,6 +75,7 @@ public class CipherService : ICipherService
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
|
_getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
||||||
@ -161,6 +166,7 @@ public class CipherService : ICipherService
|
|||||||
{
|
{
|
||||||
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
||||||
cipher.RevisionDate = DateTime.UtcNow;
|
cipher.RevisionDate = DateTime.UtcNow;
|
||||||
|
await ValidateViewPasswordUserAsync(cipher);
|
||||||
await _cipherRepository.ReplaceAsync(cipher);
|
await _cipherRepository.ReplaceAsync(cipher);
|
||||||
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);
|
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);
|
||||||
|
|
||||||
@ -966,4 +972,32 @@ public class CipherService : ICipherService
|
|||||||
|
|
||||||
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ValidateViewPasswordUserAsync(Cipher cipher)
|
||||||
|
{
|
||||||
|
if (cipher.Type != CipherType.Login || cipher.Data == null || !cipher.OrganizationId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var existingCipher = await _cipherRepository.GetByIdAsync(cipher.Id);
|
||||||
|
if (existingCipher == null) return;
|
||||||
|
|
||||||
|
var cipherPermissions = await _getCipherPermissionsForUserQuery.GetByOrganization(cipher.OrganizationId.Value);
|
||||||
|
// Check if user is a "hidden password" user
|
||||||
|
if (!cipherPermissions.TryGetValue(cipher.Id, out var permission) || !(permission.ViewPassword && permission.Edit))
|
||||||
|
{
|
||||||
|
// "hidden password" users may not add cipher key encryption
|
||||||
|
if (existingCipher.Key == null && cipher.Key != null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You do not have permission to add cipher key encryption.");
|
||||||
|
}
|
||||||
|
// "hidden password" users may not change passwords, TOTP codes, or passkeys, so we need to set them back to the original values
|
||||||
|
var existingCipherData = JsonSerializer.Deserialize<CipherLoginData>(existingCipher.Data);
|
||||||
|
var newCipherData = JsonSerializer.Deserialize<CipherLoginData>(cipher.Data);
|
||||||
|
newCipherData.Fido2Credentials = existingCipherData.Fido2Credentials;
|
||||||
|
newCipherData.Totp = existingCipherData.Totp;
|
||||||
|
newCipherData.Password = existingCipherData.Password;
|
||||||
|
cipher.Data = JsonSerializer.Serialize(newCipherData);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using System.Text.Json;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -9,7 +10,9 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
using Bit.Core.Vault.Queries;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Bit.Core.Vault.Services;
|
using Bit.Core.Vault.Services;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -797,6 +800,233 @@ public class CipherServiceTests
|
|||||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SaveDetailsAsyncDependencies
|
||||||
|
{
|
||||||
|
public CipherDetails CipherDetails { get; set; }
|
||||||
|
public SutProvider<CipherService> SutProvider { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SaveDetailsAsyncDependencies GetSaveDetailsAsyncDependencies(
|
||||||
|
SutProvider<CipherService> sutProvider,
|
||||||
|
string newPassword,
|
||||||
|
bool viewPassword,
|
||||||
|
bool editPermission,
|
||||||
|
string? key = null,
|
||||||
|
string? totp = null,
|
||||||
|
CipherLoginFido2CredentialData[]? passkeys = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var cipherDetails = new CipherDetails
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
OrganizationId = Guid.NewGuid(),
|
||||||
|
Type = CipherType.Login,
|
||||||
|
UserId = Guid.NewGuid(),
|
||||||
|
RevisionDate = DateTime.UtcNow,
|
||||||
|
Key = key,
|
||||||
|
};
|
||||||
|
|
||||||
|
var newLoginData = new CipherLoginData { Username = "user", Password = newPassword, Totp = totp, Fido2Credentials = passkeys };
|
||||||
|
cipherDetails.Data = JsonSerializer.Serialize(newLoginData);
|
||||||
|
|
||||||
|
var existingCipher = new Cipher
|
||||||
|
{
|
||||||
|
Id = cipherDetails.Id,
|
||||||
|
Data = JsonSerializer.Serialize(
|
||||||
|
new CipherLoginData
|
||||||
|
{
|
||||||
|
Username = "user",
|
||||||
|
Password = "OriginalPassword",
|
||||||
|
Totp = "OriginalTotp",
|
||||||
|
Fido2Credentials = []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.GetByIdAsync(cipherDetails.Id)
|
||||||
|
.Returns(existingCipher);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.ReplaceAsync(Arg.Any<CipherDetails>())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var permissions = new Dictionary<Guid, OrganizationCipherPermission>
|
||||||
|
{
|
||||||
|
{ cipherDetails.Id, new OrganizationCipherPermission { ViewPassword = viewPassword, Edit = editPermission } }
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>()
|
||||||
|
.GetByOrganization(cipherDetails.OrganizationId.Value)
|
||||||
|
.Returns(permissions);
|
||||||
|
|
||||||
|
return new SaveDetailsAsyncDependencies
|
||||||
|
{
|
||||||
|
CipherDetails = cipherDetails,
|
||||||
|
SutProvider = sutProvider,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PasswordNotChangedWithoutViewPasswordPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: true);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("OriginalPassword", updatedLoginData.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PasswordNotChangedWithoutEditPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("OriginalPassword", updatedLoginData.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PasswordChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("NewPassword", updatedLoginData.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_CipherKeyChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, "NewKey");
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
Assert.Equal("NewKey", deps.CipherDetails.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_CipherKeyChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, "NewKey");
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true));
|
||||||
|
|
||||||
|
Assert.Contains("do not have permission", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_TotpChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, totp: "NewTotp");
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("OriginalTotp", updatedLoginData.Totp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_TotpChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, totp: "NewTotp");
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("NewTotp", updatedLoginData.Totp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_Fido2CredentialsChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var passkeys = new[]
|
||||||
|
{
|
||||||
|
new CipherLoginFido2CredentialData
|
||||||
|
{
|
||||||
|
CredentialId = "CredentialId",
|
||||||
|
UserHandle = "UserHandle",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, passkeys: passkeys);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Empty(updatedLoginData.Fido2Credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_Fido2CredentialsChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var passkeys = new[]
|
||||||
|
{
|
||||||
|
new CipherLoginFido2CredentialData
|
||||||
|
{
|
||||||
|
CredentialId = "CredentialId",
|
||||||
|
UserHandle = "UserHandle",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, passkeys: passkeys);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal(passkeys.Length, updatedLoginData.Fido2Credentials.Length);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(
|
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user