1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 13:10:17 +00:00

Fix exporting cipher attachments with the same filename

In case a cipher has 2 attachments with the same filename, they were overwritten within the zip export

This appends an underscore as a separator and the attachmentId to truly distinguish the files by name
This commit is contained in:
Daniel James Smith
2025-04-24 15:07:56 +02:00
parent f521afa3ae
commit f7c38ef38a
2 changed files with 43 additions and 0 deletions

View File

@@ -379,6 +379,43 @@ describe("VaultExportService", () => {
const attachment = await zip.file("attachments/mock-id/mock-file-name")?.async("blob");
expect(attachment).toBeDefined();
});
it("If a ciphers attachments filename is already present in the zipfile then append underscore and attachmentId for the new filename", async () => {
const cipherData = new CipherData();
cipherData.id = "mock-id";
const cipherView = new CipherView(new Cipher(cipherData));
// Create 1st attachment with the same filename for the cipher
const attachmentView = new AttachmentView(new Attachment(new AttachmentData()));
attachmentView.id = "id-of-file-1";
attachmentView.fileName = "mock-file-name";
// Create 2nd attachment with the same filename for the cipher
const attachmentView2 = new AttachmentView(new Attachment(new AttachmentData()));
attachmentView2.id = "id-of-file-2";
attachmentView2.fileName = "mock-file-name";
cipherView.attachments = [attachmentView, attachmentView2];
cipherService.getAllDecrypted.mockResolvedValue([cipherView]);
folderService.getAllDecryptedFromState.mockResolvedValue([]);
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(255));
global.fetch = jest.fn(() =>
Promise.resolve({
status: 200,
arrayBuffer: () => Promise.resolve(new ArrayBuffer(255)),
}),
) as any;
global.Request = jest.fn(() => {}) as any;
const exportedVault = await exportService.getExport("zip");
expect(exportedVault.type).toBe("application/zip");
const exportZip = exportedVault as ExportedVaultAsBlob;
const zip = await JSZip.loadAsync(exportZip.data);
const attachment = await zip.file("attachments/mock-id/mock-file-name")?.async("blob");
expect(attachment).toBeDefined();
const attachment2 = await zip
.file("attachments/mock-id/mock-file-name_id-of-file-2")
?.async("blob");
expect(attachment2).toBeDefined();
});
});
describe("password protected export", () => {

View File

@@ -117,6 +117,12 @@ export class IndividualVaultExportService
for (const attachment of cipher.attachments) {
const response = await this.downloadAttachment(cipher.id, attachment.id);
const decBuf = await this.decryptAttachment(cipher, attachment, response);
// To prevent files with the same name from overwriting each other, we append the attachment id to the file name,
// if a file with the same name already exists in the folder
if (cipherFolder.file(attachment.fileName) != null) {
attachment.fileName += `_${attachment.id}`;
}
cipherFolder.file(attachment.fileName, decBuf);
}
}