| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
GHSA-jhph-whx8-wq6p
|
MimeBleed |
2026-07-14 |
HNUfwj (coordinated disclosure) and xet7 (fix)
![]() Did send detailed report with PoC! |
|
WeKan's file-upload validation function isFileValid() in
models/fileValidation.js detects a file's real MIME type by running the Unix
file command. When the file binary is not installed on the server
(common in minimal Docker / Alpine container deployments), detectMimeFromFile()
silently returned undefined, and the code fell back to the client-supplied
fileObj.type value from the upload request.
An attacker could therefore set fileType to "image/png" to bypass the
dangerous-MIME check and upload an HTML file containing JavaScript, resulting in stored XSS
served under the application origin.
The vulnerable fallback (before the fix):
// models/fileValidation.js — detectMimeFromFile (before)
async function detectMimeFromFile(filePath) {
if (!Meteor.isServer) return undefined;
try {
const { stdout } = await asyncExecFile('file', ['--mime-type', '-b', String(filePath)]);
const mime = (stdout || '').trim().toLowerCase();
if (!mime) return undefined;
return { mime };
} catch (e) {
// Silently returns undefined when `file` is unavailable — NO warning logged
}
return undefined;
}
// isFileValid (before): falls back to the ATTACKER-CONTROLLED fileObj.type
const mimeTypeResult = await detectMimeFromFile(fileObj.path);
const detectedMime = mimeTypeResult?.mime || (fileObj.type || '').toLowerCase();
...
const dangerousMimes = new Set(['text/html','image/svg+xml','application/javascript', ...]);
if (dangerousMimes.has(detectedMime)) { // BYPASSED when fileType is spoofed to image/png
// dangerous-content scan (only runs for deny-listed types)
}
Attacker model / prerequisites: WeKan deployed without the file command
(default in minimal container images), WITH_API=true, and an authenticated user
with board write access.
Steps to reproduce (from the report):
1. Authenticate: POST /users/login to obtain authToken.
2. Create a malicious HTML file:
<html><body><script>alert('XSS')</script></body></html>
3. Base64-encode it.
4. POST /api/attachment/upload with:
{ "fileType": "image/png", "fileData": "<base64 html>",
"boardId": "<board>", "cardId": "<card>" }
5. Server returns 200 OK — file stored without dangerous-content scanning.
6. Accessing the attachment serves HTML+JS under the WeKan origin.
Impact: stored XSS under the WeKan application origin — JavaScript execution in other users' browsers, session/authentication-token theft, and actions performed as the victim, including admin operations if an admin opens the malicious attachment.
The client-supplied MIME type can no longer gate the safety scan. When content-based detection
via the file command is unavailable, WeKan falls back to a dependency-free JS
content sniff, looksLikeDangerousMarkup(), that inspects the real bytes for
definitive HTML / SVG / XML / <script> signatures and forces the
dangerous-content scan regardless of the claimed MIME type. So a spoofed
image/png that is actually HTML + JavaScript is detected and rejected. The sniff
matches only definitive markup signatures, so genuine binary uploads (real PNG / JPEG / PDF,
including large ones) are unaffected. WeKan also now logs a one-time warning when the
file command is missing (previously the failure was silent).
// models/fileValidation.js (after)
export function looksLikeDangerousMarkup(text) {
if (!text) return false;
if (/<\s*(script|html|svg|iframe|object|embed|foreignobject|meta\b)/i.test(text)) return true;
if (/^[\s]*(<\?xml|<!doctype)/i.test(text)) return true;
if (/<!entity/i.test(text)) return true;
return false;
}
// When `file` is unavailable, sniff the real bytes and force the scan:
let contentLooksDangerous = false;
if (!mimeTypeResult) {
const { text } = await readTextHead(fileObj.path, 65536);
contentLooksDangerous = looksLikeDangerousMarkup(text);
}
if (dangerousMimes.has(detectedMime) || contentLooksDangerous) {
const allowed = await checkDangerousMimeAllowance(detectedMime, fileObj.path, fileObj.size || 0);
if (!allowed) return false; // spoofed image/png HTML+JS is rejected here
}
A regression test (server/lib/tests/fileValidationBypass.security.tests.js) covers
both the spoofed dangerous uploads (HTML, <script>, SVG, XML, DOCTYPE,
iframe/object/embed, ENTITY) and safe binaries (PNG / JPEG / PDF magic bytes, plain text, CSV,
JSON). Operators are also advised to install the file package for full
content-based MIME detection; WeKan's official images already include it.
| Timeline | Details |
|---|---|
| 2026-07-14 |
Report received from HNUfwj (coordinated disclosure, GHSA-jhph-whx8-wq6p). |
| upcoming release | Fixed at
upcoming WeKan release,
by sniffing file content in dependency-free JS when the file binary is
unavailable, so the client-supplied MIME type can no longer skip the dangerous-content scan.
|