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

Contents / RelicAvatarBleed

CVE Vulnerability name Date Responsible Security Disclosure by Vulnerabilities
-

RelicAvatarBleed

2026-09-09 xet7

Found and fixed during an internal security review.
  • RelicAvatarBleed — serveLegacyAvatar() in server/routes/avatarServer.js streams an avatar from the legacy CollectionFS store with no authentication or authorization check at either of its two call sites; the /cfs/files/avatars/:fileName route had no check at all, and the /cdn/storage/avatars/:fileName fallback ran it before WeKan's own auth checks (CWE-862 Missing Authorization)
  • Both call sites now require a signed-in caller (isLegacyAvatarAuthorized) before reading the legacy store; the anonymous public-board redirect for already-migrated avatars on /cfs/files/avatars is untouched
  • Fixed at upcoming WeKan release


Details

RelicAvatarBleed — a fallback route to a store nothing authorizes (CWE-862)

WeKan migrated its file storage from CollectionFS to ostrio:files. Some installs upgraded from a version old enough to still carry avatar data in the legacy cfs.avatars.filerecord/cfs_gridfs.avatars collections, and serveLegacyAvatar() exists to read that data in place without migrating it:

// server/routes/avatarServer.js — before
async function serveLegacyAvatar(fileId, req, res) {
  const legacy = await getOldAttachmentData(fileId, 'avatars');   // no user context at all
  if (!legacy) return false;
  const stream = await getOldAttachmentStream(fileId, 'avatars');
  ...
  res.writeHead(200);
  stream.pipe(res);                                               // served to anyone
  return true;
}
    

Two routes called it. On /cdn/storage/avatars/:fileName, it ran as a fallback when the id was not a current Avatars document - BEFORE the route's own getUserIdFromRequest/avatarIsOnAPublicBoard check further down, so a legacy id bypassed even the public-board restriction current avatars get. On /cfs/files/avatars/:fileName, it was the PRIMARY path, called with no credential check whatsoever:

// server/routes/avatarServer.js — before
WebApp.handlers.use('/cfs/files/avatars/:fileName', async (req, res, next) => {
  ...
  if (await serveLegacyAvatar(fileId, req, res)) return;   // nothing gates this
  ... // redirect to the new URL format otherwise
});
    

getOldAttachmentData (models/lib/attachmentBackwardCompatibility.js) looks the record up by _id directly, with no user context - the lookup always succeeds for an anonymous caller when the record exists. So the exploit was one anonymous request, given only an old file-record id (ids that were embedded in historical profile.avatarUrl values of the form /cfs/files/avatars/<id>):

GET /cfs/files/avatars/<legacyFileRecordId>
    

returning the avatar bytes with a one-year public cache header, no login required - on any instance migrated from WeKan older than 6.x that still has legacy avatar rows.

The fix. A new isLegacyAvatarAuthorized(req) helper requires a resolved getUserIdFromRequest(req) - unlike current avatars, a legacy record carries no owner/board link that can be safely checked for the public-board exemption avatarIsOnAPublicBoard gives current avatars, so this is a deliberately NARROWER rule rather than reusing that exemption on an unverifiable claim. Both call sites now check it before reading the legacy store: the /cdn/storage/avatars fallback returns 401 for an unauthenticated caller without ever revealing whether the id exists at all, and the /cfs/files/avatars route skips straight to its existing redirect (itself authorized, since it lands back on /cdn/storage/avatars) rather than reading the legacy store for an anonymous caller - so an anonymous visitor to an already-migrated, public-board avatar's old URL still sees it normally, and only the un-migrated legacy read now requires a login.

A denied attempt on the /cdn/storage/avatars fallback is recorded through the shared security log under a new authz.legacy-avatar/RelicAvatarBleed catalog key. tests/attachmentAvatarSecurityAdvisories.test.cjs pins both call sites and, as a negative case, that the redirect fallback still works for an anonymous caller.

Found and fixed during an internal security review, confirmed against the current source. Fixed at the upcoming WeKan release.