| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
GHSA-gm7v-pc38-53jr
CVE requested
|
BoardBleed |
2026-06-11 |
0xzap (coordinated disclosure) and Claude
![]() Did send detailed report with PoC! |
|
WeKan boards are membership-scoped: a private board is only readable/writable by its active
members. The DDP collection write policies for Cards, Lists and Swimlanes authorize an update
by checking the current (pre-update) boardId of the document and asking
whether the caller has write access to that board. They never validate the
new boardId an attacker supplies.
Because every logged-in user can create their own board (where they are admin and therefore
have write access), an attacker can take a document they own and, in a single update, set its
boardId (plus swimlaneId/listId) to a victim's private
board. The allow rule sees the attacker's own source board, approves the write, and the
document is relocated into a board the attacker is not a member of and cannot even read. This
lets an unprivileged user inject arbitrary cards/lists/swimlanes (attacker-controlled titles,
descriptions, assignees, etc.) into any private board by id, defeating board-level access
control.
The Cards write policy authorized only against doc.boardId, which Meteor
populates from the stored document (the fetch: ['boardId'] array), i.e. the
source board:
// server/permissions/cards.js
Cards.allow({
async update(userId, doc, fields) {
return await canUpdateCard(userId, doc, fields); // <-- doc.boardId = SOURCE board
},
fetch: ['boardId'],
});
There was no deny rule preventing a change of boardId. The update modifier
{ $set: { boardId: <victimBoard> } } passed the allow check because at
evaluation time doc.boardId was still the attacker's own board, where
allowIsBoardMemberWithWriteAccess returns true. The identical
pattern existed for Lists (server/permissions/lists.js) and Swimlanes
(server/permissions/swimlanes.js).
The standard auto-generated Meteor collection methods /cards/update,
/lists/update and /swimlanes/update are callable by any
authenticated DDP client directly over the Meteor websocket and were gated solely by these
allow rules.
Impact: an unprivileged authenticated user could inject attacker-controlled cards,
lists and swimlanes into any private board by id — for example planting a misleading
card titled "invoice approvals are now routed to <attacker>" into a victim's restricted
board. Confirmed live against wekanteam/wekan:latest (v9.35.0): the planted card
was visible through the board owner's own authenticated REST view, so the injected document
was genuinely part of the victim's private board.
The REST API for the same operation
(PUT /api/boards/:boardId/lists/:listId/cards/:cardId with
newBoardId) correctly calls
Authentication.checkBoardWriteAccess(req.userId, newBoardId) on the
destination board (server/models/cards.js). Only the DDP allow/deny
layer, reachable directly over the Meteor websocket by any authenticated client, was
vulnerable.
A shared denyCrossBoardMove(userId, modifier) helper was added in
server/lib/utils.js, and a deny update rule was added
to each of Cards, Lists and Swimlanes
(server/permissions/cards.js, lists.js, swimlanes.js).
The deny rule inspects the update modifier: when it $sets a boardId,
the move is rejected unless the caller has write access (active, write-capable member) on the
destination board. A cross-board move is therefore now only permitted into a board
where the user is genuinely a write-capable member, while same-board edits and legitimate
moves between boards the user belongs to keep working.
| Timeline | Details |
|---|---|
| 2026-06-11 |
Report received from 0xzap (coordinated disclosure, GHSA-gm7v-pc38-53jr, CVE requested). |
| Upcoming release | Fixed at Upcoming WeKan release, by denying any DDP update that moves a Card/List/Swimlane to a destination board the caller has no write access to. |