This release includes 1 security fix for security teams reviewing exposed deployments.
Topics
+2 more
Affected surfaces
ReleasePort's take
Moderate signalThe release fixes MimeBleed stored XSS by adding proper MIME‑type validation for file uploads.
Why it matters: Addresses a high‑severity (90) stored XSS vulnerability affecting file upload handling; operators must apply the update to prevent code execution via malicious files.
Summary
AI summaryBroad release touches import.meta.url, https://wekan.fi/hall-of-fame/mimebleed/, https://github.com/wekan/wekan/security/advisories/GHSA-jhph-whx8-wq6p, and AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N.
Changes in this release
| Type | Severity | Summary | CVE |
|---|---|---|---|
| Security | Critical |
Fixes MimeBleed stored XSS via file MIME-type validation bypass. Fixes MimeBleed stored XSS via file MIME-type validation bypass. Source: llm_adapter@2026-07-15 Confidence: high |
— |
| Feature | Medium |
Adds admin panel toggles to import/export privacy controls (six optional switches). Adds admin panel toggles to import/export privacy controls (six optional switches). Source: llm_adapter@2026-07-15 Confidence: high |
— |
| Feature | Medium |
Adds admin toggle "Always show all code as plain text" to reveal hidden markdown/HTML. Adds admin toggle "Always show all code as plain text" to reveal hidden markdown/HTML. Source: llm_adapter@2026-07-15 Confidence: high |
— |
| Feature | Medium |
Adds admin toggle "Render links as plain text" to disable clickable URLs. Adds admin toggle "Render links as plain text" to disable clickable URLs. Source: llm_adapter@2026-07-15 Confidence: high |
— |
| Bugfix | Medium |
Outgoing webhooks now include card description in payload. Outgoing webhooks now include card description in payload. Source: llm_adapter@2026-07-15 Confidence: high |
— |
| Bugfix | Medium |
Admin Panel / Version now shows MongoDB storage engine even with per‑database credentials. Admin Panel / Version now shows MongoDB storage engine even with per‑database credentials. Source: llm_adapter@2026-07-15 Confidence: high |
— |
| Bugfix | Medium |
Sandstorm: Admin Panel always available in migrated grains, not just fresh ones. Sandstorm: Admin Panel always available in migrated grains, not just fresh ones. Source: llm_adapter@2026-07-15 Confidence: high |
— |
| Bugfix | Medium |
Fixes `meteor test` crash caused by yargs ESM constructs in the Meteor wrapper. Fixes `meteor test` crash caused by yargs ESM constructs in the Meteor wrapper. Source: llm_adapter@2026-07-15 Confidence: high |
— |
| Refactor | Low |
Test infrastructure: `rebuild-wekan.sh`/`.bat` now show live progress for all test paths. Test infrastructure: `rebuild-wekan.sh`/`.bat` now show live progress for all test paths. Source: llm_adapter@2026-07-15 Confidence: high |
— |
| Refactor | Low |
`Run ALL tests` reuses precompiled `.build/bundle` for the :3000 server instead of recomputing. `Run ALL tests` reuses precompiled `.build/bundle` for the :3000 server instead of recomputing. Source: llm_adapter@2026-07-15 Confidence: high |
— |
Full changelog
v9.90 2026-07-15 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUE of MimeBleed:
- MimeBleed:
file-upload MIME-type validation bypass → stored XSS on deployments without the
filebinary
(GHSA-jhph-whx8-wq6p,
CWE-434 Unrestricted Upload of File with Dangerous Type). WeKan's upload validation
(models/fileValidation.js) detects a file's real MIME type by running the Unix
filecommand. On minimal Docker/Alpine images wherefileis not installed,
detectMimeFromFile()silently returnedundefinedand the code fell back to the
client-suppliedfileObj.type. An authenticated board member (withWITH_API=true)
could therefore upload an HTML file containing JavaScript while setting
fileType: "image/png": the spoofed type is not on the dangerous-MIME deny-list, so
the dangerous-content scan was skipped and the file was stored, yielding stored XSS
served under the WeKan origin (session theft / actions as the victim, including admin).- Fixed so the client-supplied type can never gate the safety scan: when
content-based detection viafileis unavailable, WeKan now falls back to a
dependency-free JS content sniff (looksLikeDangerousMarkup()) that inspects the
real bytes for HTML/SVG/XML/<script>signatures and forces the dangerous-content
scan regardless of the claimed MIME — so a spoofedimage/pngthat is actually
HTML+JS is caught and rejected. The sniff only matches 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 thefilecommand is missing (previously
the failure was silent). A regression test
(server/lib/tests/fileValidationBypass.security.tests.js) covers both the spoofed
dangerous uploads and safe binaries. 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
filebinary andWITH_API=true;
fixed at the upcoming WeKan release. Reported by HNUfwj. Also install thefile
package for full content-based MIME detection (WeKan's official images already do).
Thanks to HNUfwj and xet7.
- Fixed so the client-supplied type can never gate the safety scan: when
and adds the following new features:
-
Admin Panel / Features / Security: import/export privacy controls. Six new
optional toggles govern how boards and user data cross the WeKan boundary:- Disable all import / Disable all export — master switches that turn off
every import / export feature (WeKan JSON, Trello, CSV/Excel, Jira, Kanboard,
NextCloud Deck, OpenProject, GitHub/GitLab/Gitea/Forgejo, board clone, and the
single-attachment export). The server rejects any such request, and the import /
export menu options are hidden in the UI. - Disable import avatars / Disable export avatars — never carry avatars
(profile pictures) into / out of WeKan. Import covers WeKan JSON import, Trello
import and external identity-provider avatar sync on login (LDAP, OIDC/OAuth2),
gated at the singlelocalizeAvatarFromBufferchoke point; export covers WeKan
JSON and CSV export. - Anonymize import users / Anonymize export users — replace every user's
username, full name and initials with counter placeholders (user1, user2, ...),
drop their avatar, and rewrite@usernamementions plus the requested-by /
assigned-by fields inside card and comment content, so the imported board /
exported file carries no real user identity. The placeholder word "user" follows
the language of the person importing/exporting (e.g. "käyttäjä1" in Finnish). Both
export paths are covered — the in-memorybuild()and the streaming
buildStream()(which does a lightweight id-only pre-scan so mentions streamed
before the users array still resolve to matching labels).
All six default to off (current behaviour). Enforcement lives server-side in the
Exporter, the WekanCreator import path and the avatar localizer, so it cannot be
bypassed from the client. Thanks to xet7. - Disable all import / Disable all export — master switches that turn off
-
Admin Panel / Features / Security: new optional "Always show all code as plain
text" toggle. When enabled, rich text is never rendered as markdown or HTML —
the entire source is shown as escaped plain text in every rich text field (board
and card titles, descriptions, comments, checklists, etc.), so hidden content is
always revealed: HTML comments (<!-- -->), the target URL inside a markdown
link, JavaScript and any other code. All code is always visible, not clickable,
and not running. This extends the existing invisiblebleed protection (which
already showed raw source for description-less markdown links) to all content.
Thewekan-markdownpackage cannot import app code, so the setting is bridged to
the markdown renderer through a reactive flag on the exportedMarkdownobject,
kept in sync by the rich text viewer. Stored as the globalalwaysShowCodeAsText
setting; default off, so markdown renders normally. Thanks to xet7. -
Admin Panel / Features: new optional "Render links as plain text" security
toggle. When enabled, all links — both markdown links like[label](url)and
raw HTML<a href>tags — are always shown as plain, non-clickable text in every
rich text field (board and card titles, descriptions, comments, checklists,
etc.), so a link can never be clicked and cannot present misleading anchor text.
This is a hardening option on top of the existing XSS sanitization (which already
stripsjavascript:/data:schemes, event-handler attributes and dangerous
tags): it addresses #6453, where a
board title could render as a clickable link. The toggle lives under Admin Panel /
Features / Security, is stored as the globalrenderLinksAsPlainTextsetting, and
defaults to off (links stay clickable). Implemented by forbidding the<a>tag
(while keeping its visible text) in the shared DOMPurify sanitizer used by the
rich text viewer, gated on the setting so toggling it re-renders reactively.
Thanks to bcook-konza and xet7.
and adds the following updates:
- Outgoing webhooks / notifications: include the card description
(#5143). A card's description was
saved to the activity log (aa-changedDescriptionactivity, #5482) but was never
put into the outgoing-webhook / notification payload, so integrations received an
event with no description text. The payload now carriesdescription(the card's
current description, added only when non-empty) for every card event, and — for a
description change — the before/after text asoldValue/value(previously
onlytimeValue/timeOldValuewere forwarded). New fields are additive, so
existing webhook consumers are unaffected. Thanks to xet7. - Update Sandstorm Docs about how to install newest test version.
Thanks to xet7.
and fixes the following tests:
-
Test infrastructure: fix
meteor test(Mocha, server-side) crashing at boot
with 0 tests run (scripts/patch-yargs-dirname.cjs). The wholeyargspackage
is dragged into the Meteor test server bundle as a transitive devDependency,
and Meteor 3 wraps every module in a CommonJS function
function(require, exports, module, __filename, __dirname){…}. yargs (and its ESM
dependencies) ship native-ESM constructs that are illegal inside that wrapper and
crash the bundle before a single test runs:import.meta.url/import.meta.resolve(…)→ "Cannot use 'import.meta' outside a module"const __dirname = …→ "Identifier '__dirname' has already been declared"const require = createRequire(import.meta.url)(and the guarded bundler variant
const require = createRequire ? createRequire(import.meta.url) : undefined) →
"Identifier 'require' has already been declared"
A
postinstallpatch (patch-yargs-dirname.cjs) rewrites those constructs to the
wrapper's own__filename/__dirname/require(yargs is never executed here —
it only needs to parse as CommonJS). Getting the patch right took several iterations,
each of which failed in a way that looked like an unrelated source-level syntax error:- The patch expanded
import.meta.urlbefore stripping
const require = createRequire(…). The expansion injected arequire('url')…
call whose)then terminated the paren-naivecreateRequire\([^)]*\)match early,
leaving a dangling.pathToFileURL(__filename).href))— reify then died with
SyntaxError: Unexpected token (19:33)on the generated yargsesm.mjs, not on
any repo file. Diagnosing it required instrumenting reify's Babel parser in the
Meteor dev_bundle copy to dump the exact source string it was choking on, because
every scan of the repo's own*.jscame back clean (the broken file was in
node_modules, was.mjs, and was produced by the patch itself). Fixed by
removing thecreateRequireline before expandingimport.meta.url, plus a
repair rule that collapses any already-corrupted…href))leftover back to a marker. - With parsing fixed, a second ESM shim surfaced at boot — this time in a yargs
dependency,node_modules/yargs-parser/build/lib/index.js(not under
node_modules/yargs, soimport.meta.urlthere was still unexpanded, a tell that
the old yargs-only scan had never touched it). Its line 30 used the guarded ternary
const require = createRequire ? createRequire(import.meta.url) : undefined;, whose
createRequire ?(space, not() slipped past the directcreateRequire\(matcher
→ the leftoverconst requirecollided with the wrapper parameter and the server
bundle crashed at boot with "Identifier 'require' has already been declared".
Two changes fixed it: (a) generalize the removal (and its trigger) from
const require = createRequire\(…\)to a line-wiseconst require = …createRequire…;
so it matches both the direct call and the guarded-ternary form, running it before
theimport.meta.urlexpansion so the injectedrequire('url')…parens can never
confuse it; and (b) broaden the scan from justnode_modules/yargsto yargs and
its ESM dependency packages (yargs-parseris the actual offender; cliui, escalade,
string-width, y18n, get-caller-file, require-directory are scanned too and are
harmless to include).
The patch is idempotent, best-effort (never fails an install), and re-runnable by hand
(node scripts/patch-yargs-dirname.cjs) to repair anode_modulestree left broken by
an older version. This is a pre-existing test-only build breakage (the yargs bundling
predates these fixes); production runtime bundles were never affected. Separately, three
server test files were present on disk but missing from the curated loader
server/lib/tests/index.js(which themeteor testentry imports explicitly rather than
by*.tests.jsconvention), so they had silently never run — the MimeBleed file-validation
bypass regression, the import/export privacy settings, and the impersonation report query
are now registered. With all of the above, the server suite boots and runs clean:
450 passing, 0 failing (411 before the three files were wired in). Thanks to xet7. -
Test infrastructure:
rebuild-wekan.shandrebuild-wekan.batnow show live
progress in every test path. Several places used to sit silent for minutes, which
looked like a hang. Audited both scripts and closed every gap:- Server-start wait (both "Run ALL tests" modes). While the
:3000server came
up, the readiness wait printed only dots..shnow shows live progress (see the
.build/bundleitem below, where it streams the boot log scrolling);.batprints a
check counter now and then and points at the live server log totypein another
window (on cmd, echoing arbitrary log lines is unsafe as they can contain> < | &). - Fixed the readiness poll hanging so nothing showed for minutes (the progress
line froze at[0s]). The wait polledcurl http://127.0.0.1:3000/sign-inwith no
timeout; Meteor binds the :3000 proxy early and accepts the TCP connection while the
app is still building but sends no HTTP response until it finishes, socurlblocked
on that first connection for the whole build and the loop never advanced. Added
--connect-timeout 2 --max-time 4so each poll returns quickly, and the wait is now
bounded by wall-clock time (.sh1200s /.bat~240 polls) rather than a fixed count
(also applied to the Playwright-ALL precheck). - Sequential per-job (menu option 2). Each job used to block/redirect to a log
with nothing on screen while it built and ran. Because only one suite runs at a
time in this mode,.shnow streams each suite's reporter output straight to the
console viatee(also saving the log), so you see every test tick by one-by-one
as Mocha / Playwright / the E2E harness prints it, then a final PASS/FAIL + count
line;.batruns each job in its own minimized window (reusing the proven parallel
start-commands +.done-<key>flags) and polls a live pass counter, one at a time. - Playwright "ALL browsers" single menu item (
.shoption 10 /
run_playwright_parallel). It redirected each browser to a log and only dumped the
output after all finished. It now streams each browser's Playwrightlistreporter
live viatee(progress visible per test) while still saving the log;
PIPESTATUS[0]keeps the per-browser pass/fail accurate through the pipe. - The single-suite menu items (Mocha, import regression, Node E2E, single Playwright
browser) already stream straight to the terminal, and the parallel-mode combined
progress table is unchanged. Thanks to xet7.
- Server-start wait (both "Run ALL tests" modes). While the
-
Test infrastructure: "Run ALL tests" reuses the precompiled
.build/bundlefor the
:3000 server instead of recompiling withmeteor run(bothrebuild-wekan.shand
rebuild-wekan.bat). Node E2E
and Playwright drive a live WeKan over HTTP; that server is now started from the
production bundle you already built withmeteor build .build --directory—node main.jsboots in seconds with no recompile, using Meteor's bundlednodeandmongod
(mongod on :3001, or an already-running MongoDB there is reused). Its one-time
programs/servernpm install and its boot log now stream live (scrolling) so you can
see exactly what is happening. Two caveats that are inherent to Meteor and are called
out in the script: (1) the bundle is run as-is, so after changing source you must
rebuild it or the tests run old code; (2) the server-side Mocha suite still uses
meteor test(its own.meteor/local-testbuild) — the in-process unit/security tests
cannot run from a production bundle, so copying the bundle would not help them. In
sequential mode the suites run strictly one at a time, each streaming its own output,
and the parallel-only combined table is skipped (only the live WeKan server + MongoDB run
alongside — they are the system under test, not parallel test jobs). Before starting, the
run checks for the.build/bundledirectory specifically (not just.build) and
builds it once withmeteor build .build --directoryif it is missing or incomplete, so
a first run with no bundle still works. The bundle server talks to themeteor
database on :3001 — the DB name Meteor's built-in mongo used undermeteor runand the
one the Playwright / Node E2E tests seed into (tests/playwright/helpers/db.js,
tests/e2e/list-regressions.js); an earlier/wekanname made the app read an empty
database while the tests seeded a different one, so every seeded test failed until the
names were aligned. Both the Bash and the Windows batch runners now use this
bundle-based :3000 server (the.batresolves Meteor's bundlednode/mongodfrom
the dev_bundle, starts mongod in a minimized window, and stops it on exit only when it
started it). Thanks to xet7.
and fixes the following bugs:
- Sandstorm: the WeKan Admin Panel is now always available in a migrated grain,
not just a freshly created one. On Sandstorm the grain owner (Sandstorm
configurepermission) is mapped to a WeKan admin, which gates the Admin Panel
(and Admin Panel / Attachments / Sandstorm, used to delete leftover files after
a migration). A brand-new grain worked becauseUsers.after.insertderives the
role for the new user, but a grain migrated from an older WeKan already contains
the user, so that insert hook never runs. Sandstorm auto-login uses
connection.setUserId()(bypassing accounts-base, soAccounts.onLoginnever
fires), and the only remaining hook — anobserveChangesonservices.sandstorm
— fires solely when that field actually changes on login. When the migrated
user's stored permissions already equal the grain's current permissions, the
login$setis a no-op with no oplog entry, so the role was never derived and
the owner had no Admin Panel. WeKan now reconciles this at grain startup (a
migrated grain reboots once migration completes): every Sandstorm user whose
stored permissions includeconfigureis granted WeKan admin. It only promotes,
never revokes, so a stale/empty stored permission set can never lock the owner
out. Thanks to xet7. - Admin Panel / Version: show the MongoDB storage engine even with
per-database credentials (no more "unknown"). The storage engine was read
only from theserverStatuscommand, which requires the cluster-level
clusterMonitorrole. A per-database WeKan user (readWriteon thewekan
database only, as in thedocs/Platforms/FOSS/Docker/Meteor3/setup) is not
authorized to run it, so the command was rejected and the field stayed
unknown. WeKan now falls back to a$collStatsaggregation (which needs only
thecollStatsaction thatread/readWritealready grant) and reads the
real engine —wiredTiger(orinMemory) — fromstorageStats. The MongoDB
compatible version and Database commit were already correct: they come from
buildInfo, which needs no special privileges. Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
Security Fixes
- GHSA-jhph-whx8-wq6p — MimeBleed file‑upload MIME validation bypass → stored XSS (CVSS:3.1 8.3 High, AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N)
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About wekan
The Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . New English strings for new features at imports/i18n/data/en.i18n.json . Non-English translations at https://app.transifex.com/wekan/wekan only.
Related context
Related tools
Beta — feedback welcome: [email protected]