| 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. |
|
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.
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 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.