Wekan logo

Wekan

Open source kanban board application built with Meteor

Alternative to: trello


About Versions (218)

v10.74

2026-08-09

Binaries in these bundles

Each bundle carries a Node.js, a FerretDB and the MongoDB Database Tools. Which source has a given CPU varies from release to release - nodejs.org builds some architectures, unofficial-builds others, and the wekan/node fork the ones neither of them does - and not every source publishes a checksum. This is what went into this release, and which downloads were checked against a published SHA256.

BundleBinaryFromVersionCheckedSHA256
arm64FerretDBwekan/FerretDBlatestverifiedb4a627780b746988…
arm64FerretDBwekan/FerretDBlatestverifiedb4a627780b746988…
arm64Node.jsnodejs.orgv24.19.0verified01443c1e1a29e531…
arm64Node.jsnodejs.orgv24.19.0verified01443c1e1a29e531…
armhfFerretDBwekan/FerretDBv1.47.0verified5d1de10a60b0fe4e…
armhfFerretDBwekan/FerretDBv1.47.0verified5d1de10a60b0fe4e…
armhfNode.jswekan/node-patchesv24.19.0verifiedb55350f3071b765a…
armhfNode.jswekan/node-patchesv24.19.0verifiedb55350f3071b765a…
armv7FerretDBwekan/FerretDBv1.47.0verified5d1de10a60b0fe4e…
armv7FerretDBwekan/FerretDBv1.47.0verified5d1de10a60b0fe4e…
armv7Node.jswekan/node-patchesv24.19.0verified8dbe0a9aa8550ad5…
armv7Node.jswekan/node-patchesv24.19.0verified8dbe0a9aa8550ad5…
i386FerretDBwekan/FerretDBv1.46.0verified0d2f948e0337e5b1…
i386FerretDBwekan/FerretDBv1.46.0verified0d2f948e0337e5b1…
i386Node.jswekan/node-patchesv24.19.0verified3b0b3bbfe27daf58…
i386Node.jswekan/node-patchesv24.19.0verified3b0b3bbfe27daf58…
mac-arm64FerretDBwekan/FerretDBlatestverifiedd4786d3c1c32ac6b…
mac-arm64FerretDBwekan/FerretDBlatestverifiedd4786d3c1c32ac6b…
mac-arm64Node.jsnodejs.orgv24.19.0verified3f1cf157479c1480…
mac-arm64Node.jsnodejs.orgv24.19.0verified3f1cf157479c1480…
ppc64leFerretDBwekan/FerretDBv1.47.0verified899846a0ab4ec17b…
ppc64leFerretDBwekan/FerretDBv1.47.0verified899846a0ab4ec17b…
ppc64leNode.jsnodejs.orgv24.19.0verifiedc510c6ce12f07010…
ppc64leNode.jsnodejs.orgv24.19.0verifiedc510c6ce12f07010…
riscv64FerretDBwekan/FerretDBv1.47.0verified08ddaefe1e40dd85…
riscv64FerretDBwekan/FerretDBv1.47.0verified08ddaefe1e40dd85…
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0verifiedcd1f14af28121480…
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0verifiedcd1f14af28121480…
s390xFerretDBwekan/FerretDBv1.47.0verified3aa8d87111fc462e…
s390xFerretDBwekan/FerretDBv1.47.0verified3aa8d87111fc462e…
s390xNode.jsnodejs.orgv24.19.0verifieda4792e65962ffa0a…
s390xNode.jsnodejs.orgv24.19.0verifieda4792e65962ffa0a…
win64FerretDBwekan/FerretDBlatestverified508dbd2f26469fc1…
win64FerretDBwekan/FerretDBlatestverified508dbd2f26469fc1…
win64Node.jsnodejs.orgv24.19.0verified57f71ab3652e797d…
win64Node.jsnodejs.orgv24.19.0verified57f71ab3652e797d…

A row saying no checksum published is not a failed check - it is a source that publishes nothing to check against. Those are the ones worth fixing at the source.

v10.74 2026-08-09 WeKan ® release

In short: four security reports from Alpastx, all of them the same mistake in four places - a rule that exists, is correct, and is not asked. PathBleed let an avatar’s owner write the on-disk path of their own avatar and have board export read that file back out, because the guard attachments have was never copied to avatars. RevokeBleed left a revoked organization, team or domain share working in the board publication, because that publication had its own copy of the visibility query and the copy ignored isActive. ParentBleed let a card on one board name a parent on a private one and had the publication deliver that private card to everybody, because nothing checked who may see the parent’s board. CommentBleed let any board member delete anyone’s comment over REST, because the object-level rule lived in a collection hook that cannot see an HTTP caller. Each fix puts the rule in ONE place that both callers use, and each comes with a plain-node suite that pins the attack and the negatives. Auditing for more of the same found five more cursors leaking cross-board content the way ParentBleed did, three more hand-written copies of the visibility query, and a comment reaction anybody on the board could put in somebody else’s name.

The features under them are two answers to “and then what”: canary tokens, which record WHO tried a permission override and from WHERE without telling them they were seen, and a daily filesystem integrity check that asks whether every stored file is still the file WeKan stored - name, date, md5, sha256, sha512 and an ed25519 signature - and warns when one changed with no record saying why. Below that: dependency updates, the two bugs Admin Panel / Problems was itself reporting, and the security tests, which now say WHICH published vulnerability they guard so a new guard can check the whole Hall of Fame list against them - 33 of 58 covered, the other 25 recorded gaps with reasons. The binaries below are v10.73’s: nothing here rebuilds them.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

This release fixes the following CRITICAL SECURITY ISSUES:

Avatars and board export - where an uploaded picture lives on disk, and what an export is allowed to read.

PathBleed: an avatar could name any file on disk, and board export read it. Thanks to Alpastx and xet7.

PathBleed - GHSA-4mxf-m8pq-xc9p, High, CWE-22 and CWE-73. Attachments blocked client-supplied versions.*.path; avatars did not. The avatar allow rule was the single line update: isOwner - being the owner let you write ANY field of your own avatar document, versions.original.path included. That field is server-managed: it says where on disk the bytes are.

Board export then read it. For every member whose avatar is a local WeKan file the exporter opens that path and embeds the bytes as base64 in profile.avatarFile, so an authenticated user could point their own avatar at /etc/passwd - or anything else under WRITABLE_PATH - export a board they are a member of, and decode the file out of the JSON. Arbitrary file read as the WeKan OS user, from any account that can own an avatar.

Both halves are closed. The WRITE: the guards attachments had now live in one module and BOTH permission files import it - avatars had gone without them precisely because each collection wrote its own copy. Avatars refuse an insert carrying versions.*.path or .storage, refuse any update touching the versions subtree, and restrict updates to the same field whitelist attachments use; being the owner is still required and no longer sufficient. The READ: nothing is read from a stored path unless it RESOLVES to somewhere inside WeKan’s own storage. That half also holds when a path is poisoned some other way

  • a document written before this fix, a restored backup, a bad migration - and it sits at the one place export turns a stored path into bytes, for attachments and avatars alike. The download path had always checked containment this way; its private copy of the function is gone, so download and export ask the same question. The streaming attachment export, which read a stored path with nothing but an existsSync, was the last place left and takes the same check - not the reported hole, since the attachment allow rule refuses a client-supplied path, but a path is only as trustworthy as every way it could have been written.

The board publication - who is sent a board, and which of its cards.

RevokeBleed: revoking an org, team or domain share did not revoke it. Thanks to Alpastx and xet7.

RevokeBleed - GHSA-gwc4-fw7p-gw58, Moderate, CWE-639 and CWE-863. isActive: false is how a board admin REVOKES a share with an organization, a team or an email domain. All Boards honoured it - it matches shares with $elemMatch: { ..., isActive: true } - so the board disappeared from the revoked user’s list and everything looked correct.

The board publication did not. That is the publication that sends the board document and its lists, swimlanes, cards, comments and attachments, and it matched shares with 'orgs.orgId': { $in: orgsIds } instead. A dotted path matches an element of the array without saying anything about that element’s other fields, so isActive was never consulted: anyone who still knew the boardId - a bookmark, a browser tab, a note - could subscribe and receive the whole private board after their access had been taken away. A revoke the primary data publication does not honour is not a revoke.

The rule was written out twice and the two copies disagreed, so it is written once now: one builder makes the $or, both Boards.userBoards and the board publication call it, and every share kind is matched with $elemMatch requiring isActive: true. includePublic: false still drops the public clause and only that clause, which is what the search over all boards needs.

ParentBleed: one card id bridged a private board into another board's feed. Thanks to Alpastx and xet7.

ParentBleed - GHSA-jvv9-498p-hxrg, Moderate, CWE-200 and CWE-862. A card’s parentId may name a card on ANOTHER board, and setting it was authorized only against the CHILD board’s write ACL. Nothing asked whether the person setting it, or the people subscribed to the child board, were allowed to see the other board at all.

The board publication walks the whole ancestor chain, because the prefix-with-full-path subtask setting renders a subtask’s complete path (#3453), and it published the complete ancestor card DOCUMENTS to every subscriber of the child board. A member who could write on shared board B and knew a card id on private board A could point a card at it and have A’s card - title, description, custom fields - delivered over DDP to people who are not members of A.

Closed at both ends, because either alone leaves half the hole open. The WRITE refuses a parent whose board the actor cannot see - one question, asked in one place with the same selectors the publication and All Boards use, so an ACTIVE org, team or domain share counts and a revoked one does not. It is enforced on the REST card create, on the REST card update, and on DDP by a deny rule covering insert as well as update, beside the cross-board MOVE deny that GHSA-gm7v-pc38-53jr added. The PUBLICATION sends only the ancestors whose board the subscriber may see; the board being published is its own answer, so an ordinary same-board subtask path is unchanged. This is the shape of check the linked-card path already made - creating a linked card requires read access to the source card’s board - applied to the field that did not have it.

Auditing for more of the same found the LINKED-CARD cursors beside the ancestor one with the identical hole and a wider blast radius: five of them, publishing the linked card, its comments, its attachments, its checklists and its checklist items. A cardType-linkedCard names a card by id exactly as parentId does, and that card may live on any board. They take the same answer - a linked card whose source board the subscriber cannot see is not sent - and they share one helper now instead of repeating the same fifteen-line preamble five times, which is what stops the sixth from being written without the check.

The REST API - what an HTTP caller may do to somebody else’s content.

CommentBleed: the REST API let any board member delete anyone's comment. Thanks to Alpastx and xet7.

CommentBleed - GHSA-pqr4-rxgp-hv2m, Moderate, CWE-639 and CWE-863. Over DDP, deleting a comment is author-or-board-admin, and a board that sets restrictCommentEditing takes even the admin’s ability away (#5906). That rule was enforced in a collection hook keyed off the Meteor userId, with an early return for the genuine server-internal callers - board copy, cleanup, migrations - that run with no authenticated user.

An HTTP request carries no Meteor userId into the invocation context. So DELETE /api/boards/:boardId/cards/:cardId/comments/:commentId, which checked only board MEMBERSHIP, reached the collection with no user, the hook took its “server-internal, trust it” path, and any normal member could delete any comment on the board: HTTP 200, the comment gone, restrictCommentEditing or not, while the same deletion over DDP was correctly refused. Harassment and evidence destruction on a shared board.

The fix does not rely on the hook seeing something it cannot see. The handler loads the comment - 404 when there is none - and applies the same rule itself, with the REST caller’s id, before removing anything. That rule is now an exported function the hooks and the handler both call, so DDP and HTTP cannot enforce different things, and it carries a 403 so a refusal answers Forbidden rather than 500. The no-userId path stays, documented for the internal callers it was written for.

and adds the following new features:

Admin Panel / Problems / Security - what an admin is told when somebody probes.

Canary tokens record who tried to override permissions, and from where. Thanks to xet7.

The security event log said what a guard DID - a request blocked, a filename sanitized - which answers “is WeKan defending itself” and not the question an admin asks next: who did that, and from where. It also could not tell a browser that got confused from somebody working through the Hall of Fame one entry at a time.

A canary is a tripwire at a point that only a permission-override attempt reaches. Ordinary use never gets there, so a trip is not noise. Three properties define one, and each is enforced by a test.

Silent. Tripping one changes nothing the caller can observe: tripCanary() always returns false and tripCanaryDeny() always true, so a call site reads as the refusal it replaces, and the REST handler re-throws the ORIGINAL error. This is not politeness - a canary that announces itself is a map of which paths are watched, and a probe would avoid them.

Bounded. A canary sits where an attacker can loop, so one database row per attempt would be a denial of service they get for free. The first trip of a (canary, actor) pair is recorded at once, the rest of the window are COUNTED, and one summary carrying the total is written when the window closes; a pair that never gives up stops writing after sixty summaries. The tracked-pair map is capped and evicts the least RECENTLY seen, so a long-running attacker is not pushed out by a passing one. A thousand attempts in a minute cost one row, and a suppressed trip costs one map lookup.

Attributed. Every event carries the account, the username, the IP address and the attempt count. The username is stored at write time on purpose

  • it is what the account was called when it tried, so a rename does not rewrite history. The address uses the same spoofing-safe rule as the login throttle, or an attacker could write a colleague’s address into the security log by sending a header.

Seventeen canaries sit at the permission checks that refuse the attempts behind BoardBleed, ParentBleed, ChecklistBleed, PathBleed and CommentBleed, and nine more cover NoSQL injection (an execution operator in a client selector, or {"$ne": null} where a typed value belongs), SQL injection (the database’s own guard now marks its refusal so the attempt reaches the admin instead of a log file), sanitization that removed something dangerous rather than merely tidying, a forged forwarded-for header, and a login lockout. All server-side: no browser, nothing to install.

Admin Panel / Problems / Security gains Username, IP address and Attempts columns, both new ones searchable - the thing an admin does with one security event is pivot on it.

The database marks the operations WeKan never issues, so an operator sees them. Thanks to xet7.

FerretDB is reached over a local socket by one application, whose driver is a Meteor 3 one. That makes a class of operations interesting by their mere presence: server-side JavaScript ($where, eval, $function), an aggregation writing into a collection ($out, $merge), dropping a database, a server-administration command. The driver does not send them, so a request that does is either a bug or somebody who reached the socket and is looking around.

internal/util/canary refuses them with the ordinary “operation not supported by this build” - the same answer an unimplemented command gets - and appends canary:<id>, which WeKan reads off the error and records with the account and the address. The package writes nothing: no file, no table, no counter, so hammering it costs one string comparison per request. The SQL guard marks its refusals the same way; it already refused a statement carrying what only injection produces, but a line in the database’s own log is not somewhere anybody looks.

On MongoDB there is no FerretDB to mark anything and these operations simply never appear, so the feature degrades to nothing rather than misbehaving.

Admin Panel / Problems / Filesystem integrity - whether the stored files are still the files WeKan stored.

A daily paced check of every stored file's name, date and four hashes. Thanks to xet7.

Attachments and avatars are files under WRITABLE_PATH, and the database holds one document per file. Nothing checked that the two still agree. A file can be replaced, truncated, back-dated or deleted by anything that reaches the filesystem - a bad restore, a sync tool, a container rebuild, a shell on the volume - and WeKan would keep serving whatever is there now.

A baseline per file now lives in the existing WeKan database (no new files under WRITABLE_PATH): path, size, modification time and md5, sha256 and sha512. Three, because md5 is what other tools print - so an admin can compare with a backup using what they already have - and because two digests over the same bytes cannot disagree: when they do, the bytes were not read the same way twice, which is a failing disk rather than a substitution, and is its own critical finding. ed25519 is the fourth check and is not a hash but a signature, answering what the digests cannot: who says these are the right hashes? Anybody who can rewrite a file can rewrite a row of hashes, so each entry is signed and verified on every scan.

The scan runs once a day, never at or above 60% CPU, with a pause between every file (50 ms, plus 20 ms per megabyte) and a fifteen-minute budget after which it stops and continues tomorrow - and reads each file once for all three digests. A run that stopped early does not report what it never reached as missing.

The finding is a change with no record saying why. A change WeKan made is reported once and re-baselined; a change nothing accounts for keeps showing until somebody looks. Crashes and downtime are in the same stream, from a heartbeat the next start reads: a first run and a clean stop record nothing, and a long gap with no clean-shutdown mark says so, with how long the server was down.

and updates the following dependencies:

  • @aws-sdk/lib-storage 3.1085.0 → 3.1104.0 — the S3 multipart uploader the optional S3 attachment storage uses.
  • markdown-it 15.0.0 — the markdown renderer behind card descriptions and comments. A major version; its breaking changes are in plugin APIs WeKan does not use.
  • @playwright/test 1.62.0 → 1.62.1 — the browser test runner, in tests/playwright only; it ships in no WeKan bundle.
  • actions/checkout 4 → 7, actions/download-artifact 4 → 8, actions/upload-artifact 4 → 7 — the GitHub Actions steps every release workflow starts and ends with. Build-time only.

Thanks to dependabot.

and fixes the following bugs:

Comment reactions - who a reaction says it belongs to.

React as yourself, not as somebody else. Thanks to xet7.

The same shape as CommentBleed, one collection over. A CardCommentReactions document holds { cardCommentId, reactions: [ { reactionCodepoint, userIds } ] } and the whole array is ONE field, whose allow rule was board membership for insert, update and remove alike. So any member could $set reactions to anything: add a colleague’s userId to a reaction they never made, or remove one they did. toggleReaction() only ever touches the caller’s own id, so no legitimate client sends anything else - the rule simply never said so.

Integrity rather than confidentiality, since reactions are visible to the whole board already, but it puts words in another person’s mouth. A deny rule now refuses an update that changes any OTHER user’s presence in any reaction. The decision compares MEMBERSHIP rather than array order, because the client rebuilds the array on every toggle and a reordered array with the same membership is the same set of reactions. The modifier forms that cannot be checked that way - $push, $pull, $addToSet, $unset, a dotted reactions.0.userIds - are refused outright. Read-only and no-comment members still may not react at all, as before.

Admin Panel / Problems / Database problems - two of its own reports, acted on.

Fix the two bugs the Database problems page was reporting. Thanks to xet7.

The page was doing its job and nobody had acted on it. moveSwimlane threw “update is not available on the server. Please use updateAsync()” four times in a week, because the default-swimlane self-heal called the synchronous Swimlanes.upsert() that Meteor 3 removed on the server; it starts the async one without waiting now, since the getter around it cannot await. moveList threw “ValidationError: Failed validation, Cannot read properties of undefined (reading ‘title’)” when a list had no title: the insert failed schema validation and collection2’s error formatter then crashed on the undefined field, so the admin saw neither the list nor the real problem. It now says “This list has no title, so it cannot be moved to another board” before the insert, and the two console.log lines that printed the title to a log nobody reads are gone.

Both errors also gained a classifier rule, so neither reads as unknown / unclassified again: they say plainly that this is WeKan’s bug rather than the database’s or the admin’s, and where to report it.

and has the following developer-facing changes:

The test suite - what it claims to guard, and what it actually does.

Security tests say which vulnerability they guard, and the list is checked. Thanks to xet7.

“Are the security tests enough to prevent what is in the Hall of Fame” was unanswerable. WeKan publishes 58 named vulnerabilities; some suites named the one they belong to, most did not, and the Hall of Fame lives in a different repository that CI never checks out - so answering it meant reading 58 pages against 300 suites by hand, which means it was never answered. A regression test that quietly stops existing is worth nothing, which is the failure mode tests/testsAreRegistered.test.cjs already caught once, when two *bleed suites had drifted out of the mocha index.

The list lives in the repository that has the tests now, and tests/securityRegressionCoverage.test.cjs keeps the two in step. Every published vulnerability is either GUARDED - named by a suite that still exists - or RECORDED, a gap with a written reason, which is this CHANGELOG’s TODO Later pattern applied to tests. The gap count is pinned, so a vulnerability cannot be published with neither a test nor a note; a gap that turns out to be guarded after all fails too, so coverage is never understated; and a file that merely REGISTERS suites is not accepted as coverage, or the guard would pass itself.

Getting there meant naming vulnerabilities in the suites that already guarded them. tests/securityMeifukun.test.cjs guards eight reports and named one: its sections are RedirectBleed, SourceBleed, LiveBleed, CasBleed, OidcBleed, MetricsBleed, ImpersonateBleed and InviteBleed - seven vulnerabilities that looked untested and were not. tests/noIdentityReplacement.test.cjs guards IdentityBleed and PatternBleed. ExportBleed, CrashBleed, MimeBleed and the four LockoutBleed suites now say so too, and the cross-board suite also checks BoardBleed’s move deny on Lists and Swimlanes, not only Cards.

The count that comes out of it: 29 of 58 published vulnerabilities have a named regression test, and 29 are recorded gaps - mostly older fixes from before WeKan tested its security fixes at all. They are not known to be unprotected; they are known to be unchecked, which is a different and more honest statement, and each one now says what it would take to close it.

The release and setup scripts - what the build menu offers, and what it still carries.

Setup: "git pull" and "git push" that finish the job, replacing "Update git". Thanks to xet7.

The build menu’s Update git did a git pull and left it there, so a contributor who used it still had to know the other half by heart. It is two entries now - one that pulls and one that pushes - and each does the whole thing, submodules included, rather than the first step of it.

releases/: delete two superseded scripts, and two exemptions that outlived them. Thanks to xet7.

Two scripts in releases/ had been replaced by the release workflow and were kept only because guards had been written to exempt them. Both the scripts and their exemptions are gone, so the guards now describe what is really there - an exemption that outlives its reason is how a check quietly stops checking.

Thanks to above GitHub users for their contributions and translators for their translations.