From c413a7e860bc4d93fe2adcf82516228570bf382d Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sun, 18 Jan 2026 19:45:44 +0200 Subject: [PATCH] Security Fix 8: MoveStorageBleed. Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7. --- models/attachments.js | 56 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/models/attachments.js b/models/attachments.js index 6e38227fd..82ad8fcbe 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -259,7 +259,26 @@ if (Meteor.isServer) { check(fileObjId, String); check(storageDestination, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const fileObj = ReactiveCache.getAttachment(fileObjId); + if (!fileObj) { + throw new Meteor.Error('attachment-not-found', 'Attachment not found'); + } + + const board = ReactiveCache.getBoard(fileObj.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + + // Allowlist storage destinations + const allowedDestinations = ['fs', 'gridfs', 's3']; + if (!allowedDestinations.includes(storageDestination)) { + throw new Meteor.Error('invalid-storage-destination', 'Invalid storage destination'); + } + moveToStorage(fileObj, storageDestination, fileStoreStrategyFactory); }, renameAttachment(fileObjId, newName) { @@ -294,7 +313,20 @@ if (Meteor.isServer) { validateAttachment(fileObjId) { check(fileObjId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const fileObj = ReactiveCache.getAttachment(fileObjId); + if (!fileObj) { + throw new Meteor.Error('attachment-not-found', 'Attachment not found'); + } + + const board = ReactiveCache.getBoard(fileObj.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + const isValid = Promise.await(isFileValid(fileObj, attachmentUploadMimeTypes, attachmentUploadSize, attachmentUploadExternalProgram)); if (!isValid) { @@ -305,11 +337,31 @@ if (Meteor.isServer) { check(fileObjId, String); check(storageDestination, String); - Meteor.call('validateAttachment', fileObjId); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } const fileObj = ReactiveCache.getAttachment(fileObjId); + if (!fileObj) { + throw new Meteor.Error('attachment-not-found', 'Attachment not found'); + } - if (fileObj) { + const board = ReactiveCache.getBoard(fileObj.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + + // Allowlist storage destinations + const allowedDestinations = ['fs', 'gridfs', 's3']; + if (!allowedDestinations.includes(storageDestination)) { + throw new Meteor.Error('invalid-storage-destination', 'Invalid storage destination'); + } + + Meteor.call('validateAttachment', fileObjId); + + const fileObjAfter = ReactiveCache.getAttachment(fileObjId); + + if (fileObjAfter) { Meteor.defer(() => Meteor.call('moveAttachmentToStorage', fileObjId, storageDestination)); } },