Wekan logo

Wekan

Open source kanban board application built with Meteor

Alternative to: trello


About Versions (218)

v9.90

2026-07-14

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 file binary (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 file command. On minimal Docker/Alpine images where file is not installed, detectMimeFromFile() silently returned undefined and the code fell back to the client-supplied fileObj.type. An authenticated board member (with WITH_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 via file is 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 spoofed image/png that 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 the file command 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 file binary and WITH_API=true; fixed at the upcoming WeKan release. Reported by HNUfwj. Also install the file package for full content-based MIME detection (WeKan’s official images already do). Thanks to HNUfwj and xet7.

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 single localizeAvatarFromBuffer choke 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 @username mentions 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-memory build() 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.

  • 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. The wekan-markdown package cannot import app code, so the setting is bridged to the markdown renderer through a reactive flag on the exported Markdown object, kept in sync by the rich text viewer. Stored as the global alwaysShowCodeAsText 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 strips javascript:/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 global renderLinksAsPlainText setting, 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 (a a-changedDescription activity, #5482) but was never put into the outgoing-webhook / notification payload, so integrations received an event with no description text. The payload now carries description (the card’s current description, added only when non-empty) for every card event, and — for a description change — the before/after text as oldValue / value (previously only timeValue/timeOldValue were 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 whole yargs package 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 postinstall patch (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:

    1. The patch expanded import.meta.url before stripping const require = createRequire(…). The expansion injected a require('url')… call whose ) then terminated the paren-naive createRequire\([^)]*\) match early, leaving a dangling .pathToFileURL(__filename).href)) — reify then died with SyntaxError: Unexpected token (19:33) on the generated yargs esm.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 *.js came back clean (the broken file was in node_modules, was .mjs, and was produced by the patch itself). Fixed by removing the createRequire line before expanding import.meta.url, plus a repair rule that collapses any already-corrupted …href)) leftover back to a marker.
    2. 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, so import.meta.url there 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 direct createRequire\( matcher → the leftover const require collided 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-wise const require = …createRequire…; so it matches both the direct call and the guarded-ternary form, running it before the import.meta.url expansion so the injected require('url')… parens can never confuse it; and (b) broaden the scan from just node_modules/yargs to yargs and its ESM dependency packages (yargs-parser is 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 a node_modules tree 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 the meteor test entry imports explicitly rather than by *.tests.js convention), 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.sh and rebuild-wekan.bat now 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 :3000 server came up, the readiness wait printed only dots. .sh now shows live progress (see the .build/bundle item below, where it streams the boot log scrolling); .bat prints a check counter now and then and points at the live server log to type in 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 polled curl http://127.0.0.1:3000/sign-in with 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, so curl blocked on that first connection for the whole build and the loop never advanced. Added --connect-timeout 2 --max-time 4 so each poll returns quickly, and the wait is now bounded by wall-clock time (.sh 1200s / .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, .sh now streams each suite’s reporter output straight to the console via tee (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; .bat runs 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 (.sh option 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 Playwright list reporter live via tee (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.
  • Test infrastructure: “Run ALL tests” reuses the precompiled .build/bundle for the :3000 server instead of recompiling with meteor run (both rebuild-wekan.sh and 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 with meteor build .build --directorynode main.js boots in seconds with no recompile, using Meteor’s bundled node and mongod (mongod on :3001, or an already-running MongoDB there is reused). Its one-time programs/server npm 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-test build) — 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/bundle directory specifically (not just .build) and builds it once with meteor build .build --directory if it is missing or incomplete, so a first run with no bundle still works. The bundle server talks to the meteor database on :3001 — the DB name Meteor’s built-in mongo used under meteor run and the one the Playwright / Node E2E tests seed into (tests/playwright/helpers/db.js, tests/e2e/list-regressions.js); an earlier /wekan name 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 .bat resolves Meteor’s bundled node / mongod from 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 configure permission) 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 because Users.after.insert derives 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, so Accounts.onLogin never fires), and the only remaining hook — an observeChanges on services.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 $set is 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 include configure is 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 the serverStatus command, which requires the cluster-level clusterMonitor role. A per-database WeKan user (readWrite on the wekan database only, as in the docs/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 $collStats aggregation (which needs only the collStats action that read/readWrite already grant) and reads the real engine — wiredTiger (or inMemory) — from storageStats. 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.