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

Contents / UploadPathBleed

CVE Vulnerability name Date Responsible Security Disclosure by Vulnerabilities
-

UploadPathBleed

2026-09-09 xet7

Found and fixed during an internal security review.
  • UploadPathBleed — Attachments overrode ostrio:files' sanitize() to an identity function and used the client-supplied fileId verbatim as the on-disk file name in namingFunction, so an anonymous upload with fileId: "../../../../tmp/pwn" could write attacker-controlled content anywhere the WeKan process can write (CWE-22 Path Traversal, CWE-434 Unrestricted Upload of File with Dangerous Type, CWE-306 Missing Authentication for Critical Function)
  • A different bug from the earlier PathBleed (GHSA-4mxf-m8pq-xc9p, an avatar owner writing their own versions.original.path, read back by board export): this one needs no account at all, and the write itself lands wherever the traversal points, not merely a database field
  • sanitize() is restored to the same whitelist models/avatars.js already used for the same tokens; namingFunction also validates the sanitized fileId against the ObjectId shape WeKan itself generates, regenerating a fresh one when it does not match
  • A blocked attempt is recorded through the shared security log (authz.upload-path), so Admin Panel → Problems shows it happened
  • Fixed at upcoming WeKan release


Details

UploadPathBleed — an identity "sanitizer" ahead of the on-disk path (CWE-22, CWE-434, CWE-306)

Attachments are stored with ostrio:files, which builds the on-disk path from a namingFunction callback and passes the id it will use through a sanitize() callback first, on every upload transport (DDP _Start/_Write and the HTTP __upload route alike):

// ostrio:files server.js (3.0.1)
opts.fileId = self.sanitize(opts.fileId, 20, 'a');   // _Start / _Write (DDP)
opts.fileId = this.sanitize(opts.fileId, 20, 'a');   // HTTP __upload
...
result.path = `${this.storagePath(result)}${nodePath.sep}${opts.FSName}${extensionWithDot}`;
    

WeKan's models/attachments.js configured both callbacks itself:

// models/attachments.js — before
namingFunction(opts) {
  ...
  fileId = opts.fileId;      // fully client-controlled on the server branch
  const ret = fileId;        // returned VERBATIM as the on-disk file name
  return ret;
},
sanitize(str, max, replacement) {
  // keep the original filename
  return str;                // identity — every character passes through
},
    

sanitize() here is never called on the human-readable file name at all - only on the internal fileId/FSName tokens the library uses to build the physical path. An identity function on that path meant a client-supplied fileId of ../../../../tmp/wekan-pwn reached result.path unchanged, and WriteStream.init() then runs fs.mkdir(..., {recursive:true}) and fs.writeFile on it with no containment check.

Neither the HTTP __upload route nor the DDP _Start/_Write methods require an account: onBeforeUpload's only checks were file-type/name related, never this.userId. The exploit is three plain HTTP requests, no credentials:

POST /cdn/storage/attachments/__upload
x-start: 1
{"file":{"name":"pwn.js","type":"text/javascript","meta":{}},
 "fileId":"../../../../tmp/wekan-pwn","chunkSize":1024,"fileLength":25}

POST /cdn/storage/attachments/__upload
x-fileid: ../../../../tmp/wekan-pwn
x-chunkid: 1


POST /cdn/storage/attachments/__upload
x-fileid: ../../../../tmp/wekan-pwn
x-eof: 1
    

The traversal write is the whole vulnerability: whatever the WeKan process can write, an anonymous caller can overwrite - Meteor bundle modules for remote code execution on the next restart, cron/systemd units, or ~/.ssh/authorized_keys for host takeover; a random target file is a straightforward, repeatable denial of service.

The fix. sanitize() is restored to a real whitelist - the same one models/avatars.js already used for its own fileId/FSName tokens - stripping everything but alphanumerics, underscore, dot and hyphen. File DISPLAY names are untouched: they go through a separate sanitizer in onBeforeUpload, unaffected by this change. namingFunction also validates the (now-sanitized) fileId against /^[a-zA-Z0-9_-]{1,40}$/ - the ObjectId shape WeKan's own upload configuration always generates (client/lib/attachmentUploadConfig.js) - and regenerates a fresh id with Random.id() if it does not match, so a malformed or unexpected value can never reach the filesystem at all.

A blocked attempt is recorded through the shared security log under a new authz.upload-path/UploadPathBleed catalog key, so Admin Panel → Problems shows it. tests/attachmentAvatarSecurityAdvisories.test.cjs pins the restored sanitizer (including that the advisory's own PoC string can no longer contain a path separator afterward), the fileId validation, and that the guard is logged.

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