mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
Fix SECURITY ISSUE 3: Unauthenticated (or any) user can update board sort.
Thanks to Siam Thanat Hack (STH) !
This commit is contained in:
parent
0a2e6a0c38
commit
ea310d7508
6 changed files with 119 additions and 23 deletions
|
|
@ -74,10 +74,9 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
stop(evt, ui) {
|
stop(evt, ui) {
|
||||||
// To attribute the new index number, we need to get the DOM element
|
// To attribute the new index number, we need to get the DOM element
|
||||||
// of the previous and the following card -- if any.
|
|
||||||
const prevBoardDom = ui.item.prev('.js-board').get(0);
|
const prevBoardDom = ui.item.prev('.js-board').get(0);
|
||||||
const nextBoardBom = ui.item.next('.js-board').get(0);
|
const nextBoardDom = ui.item.next('.js-board').get(0);
|
||||||
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardBom, 1);
|
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1);
|
||||||
|
|
||||||
const boardDomElement = ui.item.get(0);
|
const boardDomElement = ui.item.get(0);
|
||||||
const board = Blaze.getData(boardDomElement);
|
const board = Blaze.getData(boardDomElement);
|
||||||
|
|
@ -89,7 +88,10 @@ BlazeComponent.extendComponent({
|
||||||
// DOM in its initial state. The card move is then handled reactively by
|
// DOM in its initial state. The card move is then handled reactively by
|
||||||
// Blaze with the below query.
|
// Blaze with the below query.
|
||||||
$boards.sortable('cancel');
|
$boards.sortable('cancel');
|
||||||
board.move(sortIndex.base);
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
if (currentUser && typeof currentUser.setBoardSortIndex === 'function') {
|
||||||
|
currentUser.setBoardSortIndex(board._id, sortIndex.base);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -184,10 +186,13 @@ BlazeComponent.extendComponent({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ret = ReactiveCache.getBoards(query, {
|
const boards = ReactiveCache.getBoards(query, {});
|
||||||
sort: { sort: 1 /* boards default sorting */ },
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
});
|
if (currentUser && typeof currentUser.sortBoardsForUser === 'function') {
|
||||||
return ret;
|
return currentUser.sortBoardsForUser(boards);
|
||||||
|
}
|
||||||
|
// Fallback: deterministic title sort when no user mapping is available (e.g., public page)
|
||||||
|
return boards.slice().sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||||
},
|
},
|
||||||
boardLists(boardId) {
|
boardLists(boardId) {
|
||||||
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
|
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
|
||||||
|
|
|
||||||
|
|
@ -1711,9 +1711,10 @@ if (Meteor.isServer) {
|
||||||
// All logged in users are allowed to reorder boards by dragging at All Boards page and Public Boards page.
|
// All logged in users are allowed to reorder boards by dragging at All Boards page and Public Boards page.
|
||||||
Boards.allow({
|
Boards.allow({
|
||||||
update(userId, board, fieldNames) {
|
update(userId, board, fieldNames) {
|
||||||
return _.contains(fieldNames, 'sort');
|
return canUpdateBoardSort(userId, board, fieldNames);
|
||||||
},
|
},
|
||||||
fetch: [],
|
// Need members to verify membership in policy
|
||||||
|
fetch: ['members'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// The number of users that have starred this board is managed by trusted code
|
// The number of users that have starred this board is managed by trusted code
|
||||||
|
|
|
||||||
|
|
@ -809,17 +809,13 @@ Users.helpers({
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
boards() {
|
boards() {
|
||||||
return Boards.userBoards(this._id, null, {}, { sort: { sort: 1 } });
|
// Fetch unsorted; sorting is per-user via profile.boardSortIndex
|
||||||
|
return Boards.userBoards(this._id, null, {}, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
starredBoards() {
|
starredBoards() {
|
||||||
const { starredBoards = [] } = this.profile || {};
|
const { starredBoards = [] } = this.profile || {};
|
||||||
return Boards.userBoards(
|
return Boards.userBoards(this._id, false, { _id: { $in: starredBoards } }, {});
|
||||||
this._id,
|
|
||||||
false,
|
|
||||||
{ _id: { $in: starredBoards } },
|
|
||||||
{ sort: { sort: 1 } },
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hasStarred(boardId) {
|
hasStarred(boardId) {
|
||||||
|
|
@ -834,12 +830,7 @@ Users.helpers({
|
||||||
|
|
||||||
invitedBoards() {
|
invitedBoards() {
|
||||||
const { invitedBoards = [] } = this.profile || {};
|
const { invitedBoards = [] } = this.profile || {};
|
||||||
return Boards.userBoards(
|
return Boards.userBoards(this._id, false, { _id: { $in: invitedBoards } }, {});
|
||||||
this._id,
|
|
||||||
false,
|
|
||||||
{ _id: { $in: invitedBoards } },
|
|
||||||
{ sort: { sort: 1 } },
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isInvitedTo(boardId) {
|
isInvitedTo(boardId) {
|
||||||
|
|
@ -858,6 +849,32 @@ Users.helpers({
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Get per-user board sort index for a board, or null when not set
|
||||||
|
*/
|
||||||
|
getBoardSortIndex(boardId) {
|
||||||
|
const mapping = (this.profile && this.profile.boardSortIndex) || {};
|
||||||
|
const v = mapping[boardId];
|
||||||
|
return typeof v === 'number' ? v : null;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Sort an array of boards by per-user mapping; fallback to title asc
|
||||||
|
*/
|
||||||
|
sortBoardsForUser(boardsArr) {
|
||||||
|
const mapping = (this.profile && this.profile.boardSortIndex) || {};
|
||||||
|
const arr = (boardsArr || []).slice();
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
const ia = typeof mapping[a._id] === 'number' ? mapping[a._id] : Number.POSITIVE_INFINITY;
|
||||||
|
const ib = typeof mapping[b._id] === 'number' ? mapping[b._id] : Number.POSITIVE_INFINITY;
|
||||||
|
if (ia !== ib) return ia - ib;
|
||||||
|
const ta = (a.title || '').toLowerCase();
|
||||||
|
const tb = (b.title || '').toLowerCase();
|
||||||
|
if (ta < tb) return -1;
|
||||||
|
if (ta > tb) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
hasSortBy() {
|
hasSortBy() {
|
||||||
// if use doesn't have dragHandle, then we can let user to choose sort list by different order
|
// if use doesn't have dragHandle, then we can let user to choose sort list by different order
|
||||||
return !this.hasShowDesktopDragHandles();
|
return !this.hasShowDesktopDragHandles();
|
||||||
|
|
@ -1306,6 +1323,19 @@ Users.mutations({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Set per-user board sort index for a board
|
||||||
|
* Stored at profile.boardSortIndex[boardId] = sortIndex (Number)
|
||||||
|
*/
|
||||||
|
setBoardSortIndex(boardId, sortIndex) {
|
||||||
|
const mapping = (this.profile && this.profile.boardSortIndex) || {};
|
||||||
|
mapping[boardId] = sortIndex;
|
||||||
|
return {
|
||||||
|
$set: {
|
||||||
|
'profile.boardSortIndex': mapping,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
toggleAutoWidth(boardId) {
|
toggleAutoWidth(boardId) {
|
||||||
const { autoWidthBoards = {} } = this.profile || {};
|
const { autoWidthBoards = {} } = this.profile || {};
|
||||||
autoWidthBoards[boardId] = !autoWidthBoards[boardId];
|
autoWidthBoards[boardId] = !autoWidthBoards[boardId];
|
||||||
|
|
|
||||||
50
server/lib/tests/boards.security.tests.js
Normal file
50
server/lib/tests/boards.security.tests.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/* eslint-env mocha */
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { Random } from 'meteor/random';
|
||||||
|
import '../utils';
|
||||||
|
|
||||||
|
// Unit tests for canUpdateBoardSort policy
|
||||||
|
|
||||||
|
describe('boards security', function() {
|
||||||
|
describe(canUpdateBoardSort.name, function() {
|
||||||
|
it('denies anonymous updates even if fieldNames include sort', function() {
|
||||||
|
const userId = null;
|
||||||
|
const board = {
|
||||||
|
hasMember: () => true,
|
||||||
|
};
|
||||||
|
const fieldNames = ['sort'];
|
||||||
|
|
||||||
|
expect(canUpdateBoardSort(userId, board, fieldNames)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies updates by non-members', function() {
|
||||||
|
const userId = Random.id();
|
||||||
|
const board = {
|
||||||
|
hasMember: (id) => id === 'someone-else',
|
||||||
|
};
|
||||||
|
const fieldNames = ['sort'];
|
||||||
|
|
||||||
|
expect(canUpdateBoardSort(userId, board, fieldNames)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows updates when user is a member and updating sort', function() {
|
||||||
|
const userId = Random.id();
|
||||||
|
const board = {
|
||||||
|
hasMember: (id) => id === userId,
|
||||||
|
};
|
||||||
|
const fieldNames = ['sort'];
|
||||||
|
|
||||||
|
expect(canUpdateBoardSort(userId, board, fieldNames)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies updates when not updating sort', function() {
|
||||||
|
const userId = Random.id();
|
||||||
|
const board = {
|
||||||
|
hasMember: (id) => id === userId,
|
||||||
|
};
|
||||||
|
const fieldNames = ['title'];
|
||||||
|
|
||||||
|
expect(canUpdateBoardSort(userId, board, fieldNames)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
import './utils.tests';
|
import './utils.tests';
|
||||||
import './users.security.tests';
|
import './users.security.tests';
|
||||||
|
import './boards.security.tests';
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,12 @@ allowIsBoardMemberByCard = function(userId, card) {
|
||||||
const board = card.board();
|
const board = card.board();
|
||||||
return board && board.hasMember(userId);
|
return board && board.hasMember(userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Policy: can a user update a board's 'sort' field?
|
||||||
|
// Requirements:
|
||||||
|
// - user must be authenticated
|
||||||
|
// - update must include 'sort' field
|
||||||
|
// - user must be a member of the board
|
||||||
|
canUpdateBoardSort = function(userId, board, fieldNames) {
|
||||||
|
return !!userId && _.contains(fieldNames || [], 'sort') && allowIsBoardMember(userId, board);
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue