Skip to content

wekan

v9.90 Security

This release includes 1 security fix for security teams reviewing exposed deployments.

Published 12d Productivity & Wikis
✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →
This release patches 1 known CVE

Topics

docker javascript kanban meteor real-time sandstorm
+2 more
snapcraft wekan

Affected surfaces

auth rce_ssrf

ReleasePort's take

Moderate signal
editorial:auto 12d

The 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 summary

Broad 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

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
    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.

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

Track wekan

Get notified when new releases ship.

Sign up free

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.

All releases →

Related context

Related tools

Beta — feedback welcome: [email protected]