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

Contents / ParentBleed

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.
  • ParentBleed — a card's parentId could name a card on ANOTHER board, authorized only against the child board's write access (CWE-862 Missing Authorization)
  • The board publication then walked the whole ancestor chain and sent the complete ancestor card DOCUMENTS to every subscriber of the child board, with no check that any of them may see the parent's board (CWE-200 Exposure of Sensitive Information)
  • One card id therefore bridged a private board's cards — titles, descriptions, custom fields — into another board's DDP feed
  • Fixed at upcoming WeKan release


Details

ParentBleed — a subtask's path led out of the board (CWE-200, CWE-862)

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 fix: both ends, because either alone is half a fix

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 same hole, five cursors further

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.