| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
-
|
WipeBleed |
2026-09-09 |
xet7
![]() Found and fixed during an internal security review. |
|
WeKan stores attachments and avatars with ostrio:files (meteor/ostrio:files, a
FilesCollection). The package registers its own DDP methods for every collection it manages,
including a remove method named _FilesCollectionRemove_<collectionName> - so for WeKan
these are _FilesCollectionRemove_attachments and _FilesCollectionRemove_avatars.
Its only gate is a boolean the collection is configured with:
// ostrio:files server.js (3.0.1) — the method's whole body
_methods[this._methodNames._Remove] = async function (selector) {
check(selector, Match.OneOf(String, Object));
if (self.allowClientCode) {
if (self.onBeforeRemove) {
if (!(await self.onBeforeRemove.call(userFuncs, self.find(selector)))) {
throw new Meteor.Error(403, 'Not permitted!');
}
}
const count = await self.countDocuments(selector);
if (count > 0) await self.removeAsync(selector);
return count;
}
throw new Meteor.Error(405, 'Running code on a client is not allowed!');
};
Both models/attachments.js and models/avatars.js set
allowClientCode: true. Attachments had never defined onBeforeRemove at all - the
one guard the library itself offers - so the method fell straight through to
self.removeAsync(selector) with no authorization check whatsoever. Avatars DID define one, but
only to clear the removed avatar's owner's profile.avatarUrl; the function unconditionally
return true;, which is the same as having none.
This method is entirely separate from WeKan's own
Attachments.allow/Avatars.allow rules in
server/permissions/attachments.js and server/permissions/avatars.js. Those rules
gate the ordinary Meteor Collection.remove() call; removeAsync() here is called
directly on the underlying collection object, so they never run.
The exploit is three lines from any Meteor DDP client, no account required:
conn.call('_FilesCollectionRemove_attachments', [{}], (err, res) => console.log(res));
conn.call('_FilesCollectionRemove_avatars', [{}], (err, res) => console.log(res));
An empty selector {} matches every document. The method returns the count of removed
documents, unlinks every physical file from disk/GridFS/S3 (Attachments' onAfterRemove returns
falsy, so every document is unlinked), and - for avatars - blanks
profile.avatarUrl for every affected user via onBeforeRemove's own side effect,
which ran unconditionally regardless of who called it.
The fix. Attachments.onBeforeRemove is defined for the first time, alongside the
existing Attachments.allow rules it reuses (canEditAttachmentCard): it requires
this.userId, fetches every file the selector actually matches, and refuses if the selector
matches nothing, if any matched file has no board, or if the caller lacks write access to that file's board
or linked card. Avatars.onBeforeRemove gains the same shape: it requires
this.userId, then - unless the caller is a site admin (the existing admin
"delete another user's avatar" flow in client/components/users/userAvatar.js still works) -
requires every matched avatar to belong to the caller. Both log a blocked attempt through the shared
security log under a new authz.file-remove/WipeBleed catalog key, so an
administrator can see that somebody tried.
tests/attachmentAvatarSecurityAdvisories.test.cjs pins both hooks, that an anonymous caller is
refused, that an empty/non-matching selector is refused rather than treated as nothing to check, and that
the legitimate admin avatar-delete flow keeps working.
Found and fixed during an internal security review, confirmed against the current source. Fixed at the upcoming WeKan release.