| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
GHSA-jvv9-498p-hxrg
|
ParentBleed |
2026-08-09 |
Alpastx and xet7![]() Coordinated disclosure via GitHub Security Advisory GHSA-jvv9-498p-hxrg. |
|
A WeKan card can be a subtask of another card, which it records as
parentId. Subtasks are allowed to live on a different board from their parent
— that is what the Subtasks board setting is for — so parentId may
legitimately name a card elsewhere.
Setting it was authorized only against the CHILD board. If you could write on board B, you could set any card id at all as a parent, and nothing asked whether you were allowed to see the board that id belongs to.
That would have been an untidy write on its own. What made it a disclosure is the read side. When the prefix-with-full-path subtask setting is on, a subtask is rendered with its complete ancestor path, so the board publication walks the chain to the top and publishes what it finds. It published the ANCESTOR CARD DOCUMENTS, whole, to every subscriber of the child board:
// server/publications/boards.js — before
const ancestorIds = await collectAncestorIds(parentIds, ...);
return await ReactiveCache.getCards({ _id: { $in: ancestorIds } }, {}, true);
No membership check, no visibility check — the ids came from the child board's cards,
and everything they pointed at was sent. So: create a card on shared board B, PUT
{ "parentId": "<a card on private board A>" }, and every member of B
— including people with no part in A — receives A's card with its title,
description and custom fields over DDP. The attacker needs write access on B and one card id
from A, which dual membership hands them.
The write refuses a parent whose board the actor cannot see. That is one question, and
it is asked in one place (server/lib/visibleBoardIds.js) using the same
selectors as the board publication and All Boards — so an ACTIVE organization, team or
domain share counts, and one revoked by RevokeBleed's flag does
not. It is enforced on the REST card create, on the REST card update (the PUT
the report used), and over DDP by a deny rule covering insert as well as update, beside the
cross-board MOVE deny that the earlier board-move advisory
added. A parent card that does not exist is refused too; clearing or omitting a parent is
untouched.
The publication sends only the ancestors whose board the subscriber may see. The board being published is its own answer — it is already going to that subscriber — so an ordinary same-board subtask path renders exactly as before. An ancestor on a board the subscriber cannot see is simply not sent, and a chain that yields nothing publishes nothing.
Both halves matter. Fixing only the write would leave every parentId already in
the database publishing whatever it points at; fixing only the publication would leave the
cross-board reference there to be found by the next feature that reads ancestors.
The report named the ancestor cursor. The LINKED-CARD cursors beside it had the identical
bug and a wider blast radius: five of them, publishing the linked card, its comments, its
attachments, its checklists and its checklist items. A cardType-linkedCard
names a card by id exactly as parentId does, and that card may live on any
board. Creating one requires read access to that board — but the person who created it
is not the person the publication is sending to, so every subscriber of the board carrying
the linked card was getting the source card's full document and all of its children,
whether or not they may see the board it lives on.
They take the same answer: a linked card whose source board the subscriber cannot see is not sent. The five cursors repeated the same fifteen-line preamble verbatim; they share one helper now, memoized per board, which is also what stops the sixth from being written without the check.
This is not a new kind of check for WeKan — it is the check the linked-card path
already made. Creating a linked card requires read access to the SOURCE card's board, and has
since that feature was added. parentId simply never got the same treatment.
tests/crossBoardParentCardLeak.test.cjs pins the filtered cursor, that the
unfiltered publish of every ancestor is gone, that an empty result publishes nothing, that
both REST paths validate BEFORE they write, that the deny rule covers insert as well as
update, and that clearing a parent and the pre-existing cross-board move deny still work.
Reported by Alpastx via GitHub Security
Advisory
GHSA-jvv9-498p-hxrg,
confirmed against v10.73.0. Fixed at the
upcoming WeKan release.