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

Contents / MimeBleed

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!
  • 1. MimeBleed — file-upload MIME-type validation bypass leading to stored XSS via a missing system binary fallback (CWE-434 Unrestricted Upload of File with Dangerous Type)
  • CVSS:3.1 8.3 High (AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N)
  • Affected container deployments without the file binary and WITH_API=true
  • Fixed at upcoming WeKan release


Details

1. MimeBleed — file-upload MIME-type validation bypass → stored XSS via missing system binary fallback (CWE-434)

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.

Fix

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.


Back to Hall of Fame Contents Back to Wekan Website