Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Api/Vault/Controllers/CiphersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1332,6 +1332,11 @@ await _cipherRepository.GetOrganizationDetailsByIdAsync(id) :
throw new BadRequestException($"Max file size is {CipherService.MAX_FILE_SIZE_READABLE}.");
}

if (cipher.GetAttachments().Values.Any(v => v.FileName == request.FileName))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: GetAttachments() returns null for a cipher with no existing attachments, so .Values throws NullReferenceException on the first attachment upload (the common case).

Details and fix

Cipher.GetAttachments() returns null when Attachments is null/empty (see src/Core/Vault/Entities/Cipher.cs:48-51). Calling .Values directly on the null result throws on the most common path: uploading the first attachment to a cipher.

Every other caller in the codebase null-guards this (e.g. cipher.GetAttachments() ?? new Dictionary<...> in CipherService.cs:409, attachments?.Count in CipherRequestModel.cs:162). Suggested fix:

var existingAttachments = cipher.GetAttachments();
if (existingAttachments != null && existingAttachments.Values.Any(v => v.FileName == request.FileName))
{
    throw new BadRequestException("Items cannot have multiple attachments with the same file name");
}

The added test only exercises the case where an attachment already exists, so it would not catch this regression. Consider adding a test for uploading to a cipher with no existing attachments.

{
throw new BadRequestException("Items cannot have multiple attachments with the same file name");
}

var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher,
request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id, request.LastKnownRevisionDate);

Expand Down
33 changes: 33 additions & 0 deletions test/Api.Test/Vault/Controllers/CiphersControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2486,4 +2486,37 @@ public async Task DownloadAttachmentAsync_ValidToken_ReturnsFile(
Assert.Equal(fileName, fileResult.FileDownloadName);
Assert.Same(stream, fileResult.FileStream);
}

[Theory, BitAutoData]
public async Task PostAttachment_WithDuplicateFilename_ThrowsBadRequestError(
User user, Guid cipherId, string attachmentId,
SutProvider<CiphersController> sutProvider)
{
var fileName = "shared-filename.txt";
var metaData = new CipherAttachment.MetaData
{
AttachmentId = attachmentId,
FileName = fileName,
Size = 10,
};

sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);

var cipherDetails = new CipherDetails { Id = cipherId, UserId = user.Id, Type = CipherType.Login, Data = "{}", Attachments = JsonSerializer.Serialize(
new Dictionary<string, CipherAttachment.MetaData> { { attachmentId, metaData } })};
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId, user.Id)
.Returns(Task.FromResult(cipherDetails));

var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.PostAttachment(cipherId, new AttachmentRequestModel {
AdminRequest = false,
FileName = fileName,
FileSize = 10,
Key = "test-key"
})
);
Assert.Equal("Items cannot have multiple attachments with the same file name", exception.Message);
}
}
Loading