docs/Features/Filename/Filename.md
Status: Implemented · Owner: xet7 · Related: models/attachments.server.js,
models/avatars.server.js, models/fileValidation.js, models/lib/fileStoreStrategy.js,
imports/lib/fileNameDisplay.js
This document specifies one unified subsystem for safe filenames in WeKan. It has three sides — display, upload, and existing files — that share the same small set of general functions so a filename is treated identically everywhere it is shown, stored, or served.
The motivation is concrete: a user was helped who could not open downloaded files by double-clicking because the files had the wrong extension; the extension had to be detected and fixed by hand. On top of that, filenames are an attack surface (invisible characters, right-to-left bidi spoofing, homoglyph typosquatting, HTML/JS/XML markup, XML-loop "billion laughs", path traversal, oversized names, and even harmless-but-must-block virus test files). This subsystem handles all of that automatically.
ABC,
ligatures, circled forms, …) plus confusable-homoglyph folding: characters from other
scripts that imitate ASCII (Cyrillic а, Greek ο, …) are folded to the ASCII letter they
imitate, so typosquatting names like pаypal.exe are shown as paypal.exe. Folding is
applied only inside a predominantly-Latin name (measured on the base, excluding the
extension), so a genuine non-Latin filename (all-Cyrillic, all-Greek, CJK, …) is preserved.<script>, <svg>, <!DOCTYPE>, <!ENTITY>),
php/asp processing instructions, CDATA, template-injection payloads, and dangerous URI schemes —
so the shown name is always plain, readable text. (Blaze {{ }} also HTML-escapes; this removes
the markup text as well.)cleanFileName() — surfaced as global Blaze helpers
{{cleanFilename}} (display) and {{downloadFilename}} (download, never empty) and used by card
attachments, the admin Files report, and the server download Content-Disposition header.The earlier iterations of this feature (a whole-red string; then a red warning triangle + inline per-character Unicode descriptions; then a server-side "invisible-only" filter with a toggle button and legend) were removed. There is no filter, warning, or description UI — every filename is just always shown clean.
javascript:/data: URIs, inline event
handlers, null bytes, path traversal) is refused.image.png, spreadsheet.xls, document.docx, …).file
command + mime-types), so double-clicking the downloaded file opens the right application. A
PNG named foo or foo.txt becomes foo.png; a correct .jpg for image/jpeg is kept.WRITABLE_PATH/files/temp, running file on it, then
deleting the temp file. Reuses the same streaming approach as attachment migration/move, as
one general function. Surfaced as an admin-triggerable, bounded batch corrector.<!DOCTYPE/
<!ENTITY); if so, sanitize the (small) document; otherwise stream every byte through unchanged.-N (document.pdf,
document-1.pdf, …); a same-content file keeps the shared name.(Attachments.insertAsync-based migrations — the old CollectionFS→Meteor-Files path — run through
onAfterUpload, so they get 6–11 automatically; the storage-move path moveToStorage /
copyFile implements 15–17 directly.)
| Function | Module | Used by |
|---|---|---|
cleanFileName(name) | imports/lib/fileNameDisplay.js | {{cleanFilename}} / {{downloadFilename}}, upload sanitize, viewer title |
sanitizeDownloadFileName(name) | imports/lib/fileNameDisplay.js | download Content-Disposition in both file servers |
sanitizeUploadFileName(name, mime) | models/lib/uploadFileName.js | attachment + avatar onAfterUpload, existing-file corrector |
filenameLooksLikeExploit(name) | models/lib/uploadFileName.js | isFileValid, onAfterUpload |
sanitizeUploadedFileExploits(fileObj) | models/lib/sanitizeUploadedFile.js | attachment + avatar onAfterUpload (SVG JS/XML-loop strip) |
looksLikeMalwareTestFile(text) | models/fileValidation.js | isFileValid (EICAR) |
detectStoredFileMime / correctedNameForStoredFile | models/lib/fileTypeCorrection.js | onAfterUpload, existing-file corrector |
finalizeStoredFileName (detect type + fix ext/length + number collisions) | models/lib/fileTypeCorrection.js | moveToStorage (migrations) |
hasEnoughDiskSpace(dir, bytes) | models/lib/diskSpace.js | moveToStorage |
createServeSanitizer(name) | models/lib/serveFileSanitizer.js | httpStreamOutput (single download choke point) |
Upload (Attachments.onAfterUpload / Avatars.onAfterUpload): sanitize file-content exploits in
place → reject exploit-looking filename → detect real MIME (small header → temp → file → delete
temp) → sanitizeUploadFileName (decode + fold homoglyphs + strip invisible/exploit + fix extension
rename if changed → isFileValid (dangerous-content scan, EICAR, size, external
scanner) → moveToStorage (disk-space check + streaming + partial cleanup; deletes the staging
source after the write is confirmed).Display: cleanFileName via the global helpers — no server round-trip, no stored mutation.
Serve existing file (httpStreamOutput): read stream from storage → createServeSanitizer
(sniff start; sanitize only if dangerous, else pass through) → HTTP response.
Correct existing extensions (correctFileExtensions.run, admin-only, bounded): for each file →
correctedNameForStoredFile (general detector) → rename if changed.
Every time a filename or file required sanitization — on upload, migration, the
existing-file corrector, or viewing/serving — it is recorded to the Security
event stream (action:'sanitized'), the same log the action:'blocked' upload
rejections already use. Each row carries:
at);userId), shown as a clickable username column
in the report;sanitizationReasons(): URL-encoded
name, invisible characters, typosquatting (look-alike characters), the specific
exploit kind (JavaScript code, XML code, XML loop (billion laughs),
server-side PHP/ASP, template injection, HTML code), wrong file type
(.txt → .png), filename too long, or empty-name-generated;"from" → "to";server/lib/fileContext.js).Catalog keys: file.sanitize (name), file.content (content exploit removed),
file.malware (EICAR). Logging is best-effort and never breaks the upload/view path.
Content-Disposition sanitizer, SVG CSP headers, and the upload content scan remain.file detection uses execFile (argv array, no shell), so a hostile filename cannot inject a
command.tests/fileNameDisplay.test.cjs — cleanFileName (decode, NFKC, homoglyph folding, invisible +
exploit removal, non-Latin preservation), sanitizeDownloadFileName, and wiring guards.tests/uploadFileName.test.cjs — sanitizeUploadFileName (append/replace/keep extension, empty →
type name, homoglyph + invisible strip, length cap) and filenameLooksLikeExploit positive +
negative.tests/diskSpace.test.cjs — unknown-free-space → permissive; known → compares need + margin.tests/fileTypeCorrection.test.cjs — bounded header streaming + partial-temp cleanup on error.tests/fileHardeningGuards.test.cjs — source guards for the upload/serve wiring, EICAR, disk-space
precheck + partial cleanup, and the serve-time XML-loop sniff.tests/playwright/specs/41-admin-newest-features.e2e.js — Files report shows clean names (decoded,
homoglyphs folded, invisible/exploit removed); no filter/warning/legend.