Hall of Fame image from https://openclipart.org/detail/120343/trophy
Back to Hall of Fame Contents Back to Wekan Website

Contents / GuestBleed

CVE Vulnerability name Date Responsible Security Disclosure by Vulnerabilities
GHSA-whxm-pxgj-7wqv

GuestBleed

2026-08-15 ybsun0215 and xet7

Coordinated disclosure via GitHub Security Advisory GHSA-whxm-pxgj-7wqv.
  • GuestBleed — the members and assignees ARRAYS on card create and card update were stored exactly as given, so a member of a private board could put any user id on a card, and GET /api/user/cards then fed that outsider the private board's card titles, ids and dates (CWE-639)
  • Two paths combine. Nothing validated that the ids in the arrays belong to the card's board, and the listing endpoint answered by CARD membership without re-checking access to the card's BOARD — so being named on a card was enough to read from a board that refuses to open
  • The outsider's direct read of the board stayed Forbidden the whole time, which is what made this quiet: every ordinary check still said no, while the "my cards" feed kept answering. It is continuous, not a single disclosure — the feed follows the card
  • The invariant already existed and was documented on the merge endpoint POST .../cards/{cardId}/members/{memberId}, which has refused a non-member with 400 since issue #5998. The array form on POST and PUT bypassed it, for both members and assignees
  • Both halves are fixed: every id in either array is checked with the same canAssignCardMember the merge endpoint uses, on single create, bulk create and update alike; and the listing filters by the boards the caller is an active member of, so a card placed before this release stops answering too
  • Fixed at upcoming WeKan release


Details

GuestBleed — named on a card, reading from a board (CWE-639)

A card can be created or updated with arrays of members and assignees:

POST /api/boards/{boardId}/lists/{listId}/cards      { "members": [""] }
PUT  /api/boards/{boardId}/lists/{listId}/cards/{id}  { "assignees": [""] }
    

They were persisted as given:

// server/models/cards.js — before
const members = req.body.members !== undefined ? coerceRestArrayParam(req.body.members) : undefined;
...
members,   // stored as-is
    

And the "my cards" feed answered by card membership alone:

// server/models/cards.js — before
const selector = { archived: false, $or: [{ members: userId }, { assignees: userId }] };
const cards = await ReactiveCache.getCards(selector, { sort: { dueAt: 1 } });
    

Neither is wrong on its own. Together they are a channel: a member of a private board writes an outsider's id onto a card, and the outsider's own feed then returns that card's title, its board, list and swimlane ids, its dates and its co-members — for as long as the id stays on the card.

The rule the merge endpoint already enforced now covers the array shapes as well:

// server/models/cards.js — after
async function assignableOnBoard(board, ids) {
  const out = [];
  for (const id of Array.isArray(ids) ? ids : []) {
    if (canAssignCardMember(board, id)) out.push(id);
  }
  return out;
}
    

An id that may not be assigned is dropped rather than refused, so a bulk edit does not fail over one stale id and the caller sees what was kept. The listing is filtered by board visibility as well, because fixing only the write path would leave every card placed before the release still answering. tests/restApiIdorBatch.test.cjs pins both.



Back to Hall of Fame Contents Back to Wekan Website