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

Contents / RevokeBleed

CVE Vulnerability name Date Responsible Security Disclosure by Vulnerabilities
GHSA-gwc4-fw7p-gw58

RevokeBleed

2026-08-09 Alpastx and xet7

Coordinated disclosure via GitHub Security Advisory GHSA-gwc4-fw7p-gw58.
  • RevokeBleed — revoking a board share with an organization, a team or an email domain did not revoke access to the board's DATA (CWE-639 Authorization Bypass Through User-Controlled Key, CWE-863 Incorrect Authorization)
  • The board disappeared from All Boards, which honours isActive, while the board publication matched shares with a dotted selector that ignores it — so a revoked user who still knew the boardId received the whole private board
  • Affected server/publications/boards.js, the publication that sends the board with its lists, swimlanes, cards, comments and attachments
  • Fixed at upcoming WeKan release


Details

RevokeBleed — the revoke switch the data publication never looked at (CWE-639, CWE-863)

A WeKan board can be shared with an organization, a team, or everybody holding an email address in a given domain. Each share is an entry in an array on the board (orgs, teams, domains), and each entry carries an isActive flag. Setting that flag to false is how a board admin REVOKES the share. It is the whole revoke mechanism.

The All Boards list asked the question correctly, with $elemMatch, which matches ONE array element against ALL of the given conditions at once:

// models/boards.js — Boards.userBoards
{ orgs: { $elemMatch: { orgId: { $in: user.orgIds() }, isActive: true } } },
    

The board publication — the one that actually sends the board document and its lists, swimlanes, cards, comments and attachments — had its own copy of the same question, and the copy was written with dotted paths:

// server/publications/boards.js — before
$or.push({ 'orgs.orgId':      { $in: orgsIds } });
$or.push({ 'teams.teamId':    { $in: teamsIds } });
$or.push({ 'domains.domain':  { $in: emailDomains } });
    

A dotted path matches if ANY element of the array has that value in that field, and says nothing whatever about that element's other fields. So isActive was never consulted here: the board vanished from the revoked user's All Boards, which is what the admin saw and believed, while a subscription by boardId still returned everything.

What that looked like

A board admin shares private board B with organization O, a member of O opens B and their browser remembers the URL, and the admin later revokes O's share. B leaves that user's board list. Opening the bookmark — or calling Meteor.subscribe('board', boardId, false) — still delivered the board document and its children, and the private card content could be read as before. The same applied to team shares and to domain shares. Whoever had once been given access kept it, for as long as they remembered one id.

The fix: one builder, both callers

The two selectors were two copies of one rule, and they disagreed. There is one copy now: models/lib/boardVisibilitySelectors.js builds the $or that answers "which boards may this user see", and both Boards.userBoards and the board publication call it. Every share kind is matched with $elemMatch requiring isActive: true, so a share counts only while it is active, everywhere.

The one clause that is not a relationship to the user — { permission: 'public' }, which means "anybody may open this" — is still optional: the boards LIST includes it, because a public board is meant to be discoverable, and the search over all boards drops it with includePublic: false, so a common word does not answer with strangers' cards.

Because the same builder is now the single answer to that question, the check that ParentBleed added for cross-board parent cards inherits it too: a revoked share does not make another board's cards visible there either.

tests/boardShareRevokeBypass.test.cjs pins each share kind's selector shape, that NO clause anywhere matches a share without requiring isActive, that the three dotted selectors are not produced at all, that an anonymous caller reaches public boards and nothing else, and that junk in the id lists is dropped rather than handed to Mongo.

Reported by Alpastx via GitHub Security Advisory GHSA-gwc4-fw7p-gw58, confirmed against v10.73.0. Fixed at the upcoming WeKan release.