From 55576ec17722db094835470b386162c9a662fb60 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sun, 18 Jan 2026 19:29:58 +0200 Subject: [PATCH] Security Fix 5: PositionHistoryBleed. Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7. --- server/methods/positionHistory.js | 127 ++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/server/methods/positionHistory.js b/server/methods/positionHistory.js index c16b874a1..704b3b9d6 100644 --- a/server/methods/positionHistory.js +++ b/server/methods/positionHistory.js @@ -4,6 +4,7 @@ import PositionHistory from '/models/positionHistory'; import Swimlanes from '/models/swimlanes'; import Lists from '/models/lists'; import Cards from '/models/cards'; +import { ReactiveCache } from '/imports/reactiveCache'; /** * Server-side methods for position history tracking @@ -15,11 +16,20 @@ Meteor.methods({ 'positionHistory.trackSwimlane'(swimlaneId) { check(swimlaneId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const swimlane = Swimlanes.findOne(swimlaneId); if (!swimlane) { throw new Meteor.Error('swimlane-not-found', 'Swimlane not found'); } + const board = ReactiveCache.getBoard(swimlane.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return swimlane.trackOriginalPosition(); }, @@ -29,11 +39,20 @@ Meteor.methods({ 'positionHistory.trackList'(listId) { check(listId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const list = Lists.findOne(listId); if (!list) { throw new Meteor.Error('list-not-found', 'List not found'); } + const board = ReactiveCache.getBoard(list.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return list.trackOriginalPosition(); }, @@ -43,11 +62,20 @@ Meteor.methods({ 'positionHistory.trackCard'(cardId) { check(cardId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const card = Cards.findOne(cardId); if (!card) { throw new Meteor.Error('card-not-found', 'Card not found'); } + const board = ReactiveCache.getBoard(card.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return card.trackOriginalPosition(); }, @@ -57,11 +85,20 @@ Meteor.methods({ 'positionHistory.getSwimlaneOriginalPosition'(swimlaneId) { check(swimlaneId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const swimlane = Swimlanes.findOne(swimlaneId); if (!swimlane) { throw new Meteor.Error('swimlane-not-found', 'Swimlane not found'); } + const board = ReactiveCache.getBoard(swimlane.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return swimlane.getOriginalPosition(); }, @@ -71,11 +108,20 @@ Meteor.methods({ 'positionHistory.getListOriginalPosition'(listId) { check(listId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const list = Lists.findOne(listId); if (!list) { throw new Meteor.Error('list-not-found', 'List not found'); } + const board = ReactiveCache.getBoard(list.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return list.getOriginalPosition(); }, @@ -85,11 +131,20 @@ Meteor.methods({ 'positionHistory.getCardOriginalPosition'(cardId) { check(cardId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const card = Cards.findOne(cardId); if (!card) { throw new Meteor.Error('card-not-found', 'Card not found'); } + const board = ReactiveCache.getBoard(card.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return card.getOriginalPosition(); }, @@ -99,11 +154,20 @@ Meteor.methods({ 'positionHistory.hasSwimlaneMoved'(swimlaneId) { check(swimlaneId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const swimlane = Swimlanes.findOne(swimlaneId); if (!swimlane) { throw new Meteor.Error('swimlane-not-found', 'Swimlane not found'); } + const board = ReactiveCache.getBoard(swimlane.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return swimlane.hasMovedFromOriginalPosition(); }, @@ -113,11 +177,20 @@ Meteor.methods({ 'positionHistory.hasListMoved'(listId) { check(listId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const list = Lists.findOne(listId); if (!list) { throw new Meteor.Error('list-not-found', 'List not found'); } + const board = ReactiveCache.getBoard(list.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return list.hasMovedFromOriginalPosition(); }, @@ -127,11 +200,20 @@ Meteor.methods({ 'positionHistory.hasCardMoved'(cardId) { check(cardId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const card = Cards.findOne(cardId); if (!card) { throw new Meteor.Error('card-not-found', 'Card not found'); } + const board = ReactiveCache.getBoard(card.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return card.hasMovedFromOriginalPosition(); }, @@ -141,11 +223,20 @@ Meteor.methods({ 'positionHistory.getSwimlaneDescription'(swimlaneId) { check(swimlaneId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const swimlane = Swimlanes.findOne(swimlaneId); if (!swimlane) { throw new Meteor.Error('swimlane-not-found', 'Swimlane not found'); } + const board = ReactiveCache.getBoard(swimlane.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return swimlane.getOriginalPositionDescription(); }, @@ -155,11 +246,20 @@ Meteor.methods({ 'positionHistory.getListDescription'(listId) { check(listId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const list = Lists.findOne(listId); if (!list) { throw new Meteor.Error('list-not-found', 'List not found'); } + const board = ReactiveCache.getBoard(list.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return list.getOriginalPositionDescription(); }, @@ -169,11 +269,20 @@ Meteor.methods({ 'positionHistory.getCardDescription'(cardId) { check(cardId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + const card = Cards.findOne(cardId); if (!card) { throw new Meteor.Error('card-not-found', 'Card not found'); } + const board = ReactiveCache.getBoard(card.boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return card.getOriginalPositionDescription(); }, @@ -183,6 +292,15 @@ Meteor.methods({ 'positionHistory.getBoardHistory'(boardId) { check(boardId, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + + const board = ReactiveCache.getBoard(boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + return PositionHistory.find({ boardId: boardId, }, { @@ -197,6 +315,15 @@ Meteor.methods({ check(boardId, String); check(entityType, String); + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You must be logged in.'); + } + + const board = ReactiveCache.getBoard(boardId); + if (!board || !board.isVisibleBy({ _id: this.userId })) { + throw new Meteor.Error('not-authorized', 'You do not have access to this board.'); + } + if (!['swimlane', 'list', 'card'].includes(entityType)) { throw new Meteor.Error('invalid-entity-type', 'Entity type must be swimlane, list, or card'); }