| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
GHSA-xm8x-c8wg-jhmf
|
SortBleed |
2026-07-13 |
5ud0 / Tarmo Technologies (coordinated disclosure) and xet7 (fix)
![]() Did send detailed report with PoC! |
|
sort allow rule (CWE-863, CWE-269)
Wekan exposes the Boards MongoDB collection to authenticated DDP clients through
Meteor allow/deny rules, so a browser can issue a direct Boards.update(selector, modifier)
over the wire. The intended board-document write rule is allowIsBoardAdmin (only a board
admin may update a board), reinforced by deny rules that forbid removing the last admin and forbid
tampering with the star count.
To support drag-to-reorder on the All Boards / Public Boards pages, a second
Boards.allow({ update }) rule was added that returns true for any board
member whenever the update touches the sort field:
// server/lib/utils.js (vulnerable)
export function canUpdateBoardSort(userId, board, fieldNames) {
return !!userId && (fieldNames || []).includes('sort') && allowIsBoardMember(userId, board);
}
// server/permissions/boards.js
Boards.allow({
update: allowIsBoardAdmin, // intended board-document write policy
remove: allowIsBoardAdmin,
fetch: ['members'],
});
Boards.allow({
update(userId, board, fieldNames) {
return canUpdateBoardSort(userId, board, fieldNames); // true if 'sort' is *among* the fields
},
fetch: ['members'],
});
Meteor evaluates allow rules with OR semantics and does not scope an approving rule to
the field that satisfied it: if any allow callback returns true and no deny callback
returns true, the entire modifier is applied to the document. Because
canUpdateBoardSort only checked that sort was one of the modified fields
(not that it was the only one), a low-privilege board member could smuggle arbitrary board
mutations into the same $set as sort. A comment-only or read-only member (a
deliberately restricted collaborator with no write access at all) could therefore rewrite
members to make themselves board admin, flip a private board's permission to
public, change the board title, and evict the legitimate owner — all with a single DDP
call:
{$set: {sort: 99, permission: 'public', title: 'PWNED', members: [{userId: attacker, isAdmin: true, isActive: true}]}}
The last-admin deny rule did not help because it only inspected $pull operations, so a
wholesale $set: { members: [...] } bypassed it entirely.
Attacker model: an authenticated Wekan user who is an active member of the target board in any role — including the most restricted roles (comment-only, read-only, worker), which are explicitly meant to have no ability to modify the board or its membership. No board-admin rights, no global-admin rights, and no victim interaction are required. The only bound on impact is that the attacker must already be a member of the board they take over.
Impact: full board-admin control over a board the attacker was granted only restricted access
to — privilege escalation (set isAdmin = true for themselves), board takeover / denial of
service to the owner ($set the whole members array to only themselves, evicting the
legitimate admin), a confidentiality breach (change permission from private to public,
exposing swimlanes, lists, cards, comments, attachments and the member list — public boards are
world-readable in Wekan), and arbitrary integrity changes (title, color, settings). Once board admin,
the attacker inherits every board-admin capability (adding integrations / outgoing webhooks,
adding/removing members, archiving or deleting the board, changing everyone's roles). Confirmed at
runtime by the reporter against a local instance (v9.84 image; the vulnerable code is byte-identical
across v9.84, v9.85 and main): the control call (a members rewrite without sort) was
correctly denied with 403 Access denied, while the identical rewrite with
sort:99 added was accepted.
The sort-reorder allow rule is now restricted to updates that touch only the sort
field (and nothing else), so it can never approve a modifier that also mutates members,
permission, title or any other field:
// server/lib/utils.js (fixed)
export function canUpdateBoardSort(userId, board, fieldNames) {
const fields = fieldNames || [];
return !!userId && fields.length === 1 && fields[0] === 'sort' && allowIsBoardMember(userId, board);
}
With sort required to be the sole modified field, the only remaining allow rule for a
multi-field board update is allowIsBoardAdmin, which a comment-only / read-only member
fails — so the whole modifier is rejected. As defense in depth, the last-admin deny rule in
server/permissions/boards.js now also rejects a $set rewrite of the
members array that would drop the last active admin, not just a $pull. A
regression test in server/lib/tests/boards.security.tests.js covers the multi-field
smuggling case (['sort', 'members'], ['sort', 'permission', 'title'], etc.).
The legitimate All Boards / Public Boards drag-reorder is unaffected: it persists the manual order
per-user in profile.boardSortIndex (Users.setBoardSortIndex), not in the
board document.
| Timeline | Details |
|---|---|
| 2026-07-13 |
Report received from 5ud0 / Tarmo Technologies (coordinated disclosure, GHSA-xm8x-c8wg-jhmf). |
| upcoming release | Fixed at
upcoming WeKan release,
by restricting the board sort allow rule to updates that touch only the
sort field, so a board member can no longer smuggle members/permission/title
changes into the same modifier.
|