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

Contents / WipeBleed

CVE Vulnerability name Date Responsible Security Disclosure by Vulnerabilities
-

WipeBleed

2026-09-09 xet7

Found and fixed during an internal security review.
  • WipeBleed — ostrio:files' own _FilesCollectionRemove_attachments/_FilesCollectionRemove_avatars DDP methods are gated only by allowClientCode and never route through Attachments.allow/Avatars.allow, so any anonymous DDP connection could call one with selector {} and delete every attachment or avatar on the instance (CWE-862 Missing Authorization)
  • Both collections now define onBeforeRemove, requiring this.userId and checking every matched file individually: attachments need the caller's board-write access, avatars need ownership of that avatar or site-admin status
  • An empty or non-matching selector is refused outright rather than treated as "nothing to check"
  • A blocked attempt is recorded through the shared security log (authz.file-remove), so Admin Panel → Problems shows it happened
  • Fixed at upcoming WeKan release


Details

WipeBleed — a file-storage library's own remove method, unauthenticated (CWE-862)

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.