mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 09:40:20 -05:00
Close the MutationBleed siblings: shared board display settings need write access.
applyListWidth, setBoardAutoWidth and setStickyListHeaders change board-wide state that every viewer sees, and refused only non-members, so read-only, comment-only and worker members could change them. They now require write access (not recorded as attempts: the menus may still offer them). applyListWidth also awaits its write, which it had fired and forgotten. The negative test fails on any server method that writes board content after a membership-only check. Thanks to xet7 !
This commit is contained in:
1 parent
215293868b
commit
fd458543f9
2 files changed
+46
-4
No files matched your search
+14
-4
@@ -1,5 +1,6 @@
|
||||
const { CALENDAR_SYSTEM_IDS } = require('/imports/lib/calendarSystems');
|
||||
const { assertSafeMapKey } = require('/models/lib/safeMapKey');
|
||||
const { memberCan } = require('/models/lib/boardRoleCapabilities');
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
// Only the authorized server method can populate this creation context.
|
||||
const adminCreation = new Meteor.EnvironmentVariable();
|
||||
@@ -1152,7 +1153,10 @@ Meteor.methods({
|
||||
// list.width/constraint are per-board fields shared with all users, so
|
||||
// only board members may change them, and only on the list's own board.
|
||||
const board = await ReactiveCache.getBoard(boardId);
|
||||
if (!board || !board.hasMember(this.userId)) {
|
||||
// MutationBleed sibling (2026-10-02): a shared board setting needs write
|
||||
// access, not just membership. Not recorded as an attempt: the menus may
|
||||
// still offer it to a read-only member.
|
||||
if (!board || !memberCan(board.members, this.userId, 'write')) {
|
||||
throw new Meteor.Error('error-notAuthorized');
|
||||
}
|
||||
const list = await ReactiveCache.getList(listId);
|
||||
@@ -1163,7 +1167,7 @@ Meteor.methods({
|
||||
// #6409: only the shared per-board width is stored on the list. The old
|
||||
// `constraint` (max-width) is no longer used; the param is kept for
|
||||
// backwards compatibility with existing callers but ignored.
|
||||
Lists.updateAsync(listId, { $set: { width: width } });
|
||||
await Lists.updateAsync(listId, { $set: { width: width } });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error updating list width:', error);
|
||||
@@ -1180,7 +1184,10 @@ Meteor.methods({
|
||||
// Shared (per-board) auto-width affects everyone, so only board members may
|
||||
// change it (parity with applyListWidth). See #6409.
|
||||
const board = await ReactiveCache.getBoard(boardId);
|
||||
if (!board || !board.hasMember(this.userId)) {
|
||||
// MutationBleed sibling (2026-10-02): a shared board setting needs write
|
||||
// access, not just membership. Not recorded as an attempt: the menus may
|
||||
// still offer it to a read-only member.
|
||||
if (!board || !memberCan(board.members, this.userId, 'write')) {
|
||||
throw new Meteor.Error('error-notAuthorized');
|
||||
}
|
||||
await Boards.updateAsync(boardId, { $set: { autoWidth: !!autoWidth } });
|
||||
@@ -1197,7 +1204,10 @@ Meteor.methods({
|
||||
throw new Meteor.Error('not-logged-in', 'User must be logged in');
|
||||
}
|
||||
const board = await ReactiveCache.getBoard(boardId);
|
||||
if (!board || !board.hasMember(this.userId)) {
|
||||
// MutationBleed sibling (2026-10-02): a shared board setting needs write
|
||||
// access, not just membership. Not recorded as an attempt: the menus may
|
||||
// still offer it to a read-only member.
|
||||
if (!board || !memberCan(board.members, this.userId, 'write')) {
|
||||
throw new Meteor.Error('error-notAuthorized');
|
||||
}
|
||||
await Boards.updateAsync(boardId, {
|
||||
|
||||
@@ -74,3 +74,35 @@ test('negative inventory: every reported sibling uses the shared write guard', (
|
||||
}
|
||||
assert.doesNotMatch(read('server/models/lists.js'), /typeof allowIsBoardMemberWithWriteAccess/);
|
||||
});
|
||||
|
||||
// MutationBleed siblings (2026-10-02): applyListWidth, setBoardAutoWidth and
|
||||
// setStickyListHeaders change shared board state every viewer sees, and
|
||||
// checked only membership, so read-only, comment-only and worker members could
|
||||
// change them. They need write access now; applyListWidth also awaits its
|
||||
// write so a failure is not lost.
|
||||
test('shared board display settings need write access, and no method writes a board after a membership-only check', () => {
|
||||
const users = read('server/models/users.js');
|
||||
for (const name of ['applyListWidth', 'setBoardAutoWidth', 'setStickyListHeaders']) {
|
||||
const at = users.indexOf(` async ${name}(`);
|
||||
const body = users.slice(at, users.indexOf('\n },', at));
|
||||
assert.match(body, /if \(!board \|\| !memberCan\(board\.members, this\.userId, 'write'\)\) \{/, name);
|
||||
assert.doesNotMatch(body, /\.hasMember\(/, name);
|
||||
}
|
||||
assert.match(users, /await Lists\.updateAsync\(listId, \{ \$set: \{ width: width \} \}\);/);
|
||||
// Negative, tree-wide: a method that writes board content after refusing
|
||||
// only non-members is the MutationBleed shape.
|
||||
const walk = dir => fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory()
|
||||
? (e.name === 'tests' || e.name.startsWith('_build') ? [] : walk(path.join(dir, e.name)))
|
||||
: (e.name.endsWith('.js') ? [path.join(dir, e.name)] : []));
|
||||
const offenders = [];
|
||||
for (const file of [...walk(path.join(root, 'server')), ...walk(path.join(root, 'models'))]) {
|
||||
for (const method of fs.readFileSync(file, 'utf8').split(/\n async (?=\w+\()/).slice(1)) {
|
||||
const body = method.slice(0, method.indexOf('\n },'));
|
||||
if (/!\s*\w+\.hasMember\(this\.userId\)/.test(body) &&
|
||||
/(Boards|Lists|Swimlanes|Cards|Checklists|CustomFields)\.(direct\.)?(updateAsync|insertAsync|removeAsync)\(/.test(body)) {
|
||||
offenders.push(`${file}: ${method.slice(0, method.indexOf('('))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(offenders, []);
|
||||
});
|
||||
Reference in new issue
Block a user