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

Contents / CommentBleed

CVE Vulnerability name Date Responsible Security Disclosure by Vulnerabilities
GHSA-pqr4-rxgp-hv2m

CommentBleed

2026-08-09 Alpastx and xet7

Coordinated disclosure via GitHub Security Advisory GHSA-pqr4-rxgp-hv2m.
  • CommentBleed — the REST comment DELETE endpoint checked board MEMBERSHIP only, so any normal member could delete any other user's comment (CWE-639 Authorization Bypass Through User-Controlled Key, CWE-863 Incorrect Authorization)
  • The author-or-admin rule, and the board's restrictCommentEditing setting, lived in a collection hook keyed off the Meteor userId — which an HTTP request does not carry, so the hook took its "server-internal, trust it" path and allowed the deletion
  • Broken object-level authorization on a collaborative board: harassment and evidence destruction, while the same deletion over DDP was correctly refused
  • Fixed at upcoming WeKan release


Details

CommentBleed — the rule was there, and the REST path never reached it (CWE-639, CWE-863)

Deleting a comment in WeKan is author-or-board-admin, and a board that switches on restrictCommentEditing takes even the admin's ability away, leaving only the author. That policy is real, it is tested, and over DDP it works.

It was enforced in a CardComments collection hook, which decides from the Meteor userId of the invocation. Genuine server-internal work — copying a board, cleanup, migrations — runs with no authenticated user, so the hook began:

// models/cardComments.js
if (!userId) {
  return;      // server-internal operation: not a user action, do not block it
}
    

An HTTP request is not a server-internal operation, but it does not carry a Meteor userId into the invocation context either. The REST handler had only the board-level check:

// server/models/cardComments.js — before
await Authentication.checkBoardAccess(req.userId, paramBoardId);
await CardComments.removeAsync({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId });
    

checkBoardAccess answers "is this person a member of this board" — a perfectly good question, and the wrong one for "may this person delete THIS comment". The removal then reached the collection with no userId, the hook took its trusted path, and the comment was gone. HTTP 200, somebody else's comment deleted, on a board with restrictCommentEditing switched on, by a member who over DDP would have been refused.

Both users only need to be ordinary members of the same board, and the API token of a normal account is enough. On a shared board that is harassment, and the destruction of a record somebody may be relying on.

The fix: ask the question where the answer is known

The repair is not to teach the hook about HTTP callers — a hook cannot see an identity that was never put in the context. It is for the handler, which DOES know who is calling, to apply the rule itself:

That rule is now an exported function (assertCanMutateComment) which the collection hooks and the REST handler both call, so DDP and HTTP cannot enforce different things — which is the actual bug, in one sentence. The pure decision underneath it (canEditComment) is unchanged, and so is DDP behaviour.

The if (!userId) path stays, because the internal callers it was written for are real and would break without it. What changed is that a REST request no longer arrives there anonymously.

tests/restCommentDeleteAcl.test.cjs pins that the handler loads the comment, that a missing one is a 404, that the ACL runs BEFORE the removal, that board membership alone is no longer sufficient, that the hooks still call the same function, and — so a future comment-edit route cannot repeat this — that no REST PUT for comments exists without the same check.

Reported by Alpastx via GitHub Security Advisory GHSA-pqr4-rxgp-hv2m, confirmed against v10.73.0 with WITH_API=true. Fixed at the upcoming WeKan release.