Wekan
Open source kanban board application built with Meteor
Alternative to: trello
v9.74
2026-07-05v9.74 2026-07-05 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of DnsBleed and ExcelBleed:
-
DnsBleed — SSRF filter bypass via DNS-resolving hostname in outgoing webhooks (GHSA-66m2-4wfr-c45p, CWE-918). Incomplete-fix follow-up to WebhookBleed (GHSA-hc3x-hq3m-663q) and IntegrationBleed / RebindBleed. The synchronous URL validator on outgoing-webhook (board Integrations) URLs in
models/integrations.jsblocks private/loopback/link-local IPs by regex-matching the URL hostname string and never resolves DNS. A public hostname that resolves to a blocked address — e.g.169-254-169-254.nip.io→169.254.169.254(cloud metadata),127-0-0-1.nip.io→127.0.0.1, or any attacker-controlled domain with an A/AAAA record pointing at an internal IP — passes that string blocklist. The report notes the underlying weakness: string matching is not an SSRF boundary because it can’t see the resolved IP.- The reported PoC was already blocked at delivery by the earlier RebindBleed fix: outgoing
webhooks are sent through
fetchSafe(server/lib/ssrfGuard.js), which resolves the hostname, validates the resolved IP, pins the connection to it and blocks redirects — so169-254-169-254.nip.iois rejected before any request is made. The REST write paths (POST/PUT /api/boards/:boardId/integrations) were likewise already hardened in WebhookBleed to run the DNS-awarevalidateAttachmentUrl()at input time. - This release completes the fix and removes the drift risk the advisory points at. The
delivery guard previously resolved only IPv4 A-records (
dns.resolve4), leaving it blind to AAAA (an IPv6-only internal target was merely fail-closed, and legitimate IPv6 webhooks were unreachable) and keeping a second, less-complete private-range block-list that could drift out of sync with the input-time one.fetchSafenow resolves both address families viadns.lookup({ all: true })— the same resolver call the input validator uses — and validates every resolved IP through the single sharedisIpBlockedblock-list inmodels/lib/attachmentUrlValidation.js. The three previously-separate block-lists (schema regex, delivery guard, input validator) are now one source of truth, and the schema-levelurlvalidator is documented in code as a non-authoritative first-line UI check only. - Affected Wekan v8.36 and later (input-side string validator); the delivery-time SSRF boundary has been in place since v8.35/v8.36 (IntegrationBleed) and v9.32 (WebhookBleed). Reported by 4n207. Thanks to 4n207 and xet7!
- The reported PoC was already blocked at delivery by the earlier RebindBleed fix: outgoing
webhooks are sent through
-
ExcelBleed — broken access control in the Excel-export REST route lets any authenticated user export any private board (GHSA-mwq8-ccpm-r533, CWE-862 / CWE-639). Same un-awaited async-auth bug class as BFLABleed (48 REST endpoints, v9.22), CloneBleed (un-awaited
allowIsBoardMemberByCard, v9.35) and TokenBleed.models/exportExcel.jscalled its access-control guardexporterExcel.canExport(user)withoutawait. BecausecanExportisasync, it returns a Promise (always truthy), soif (exporterExcel.canExport(user) || impersonateDone)was always true andexporterExcel.build(res)ran regardless of the guard’s real result — any authenticated user could download the full contents of any board (card titles + descriptions, lists, swimlanes, members, metadata) viaGET /api/boards/:boardId/exportExcel, including private boards they are not a member of. The JSON export route (/export) was correctly awaited and returned 403 for the same non-member.- Fixed by awaiting the guard —
if ((await exporterExcel.canExport(user)) || impersonateDone)— matching every other export route (models/export.js,exportPDF.js,exportExcelCard.js,import.js). The Excel-export route was the lone remaining un-awaitedcanExportcall site. - Affected Wekan v9.57.0 (latest) and earlier; present at HEAD until this release. Reported by sec-reex (defensive research, responsible disclosure, read-only PoC). Thanks to sec-reex and xet7!
- Fixed by awaiting the guard —
and adds the following updates:
- Update to Meteor 3.5. Thanks to Meteor developers.
- Run all tests at parallel or sequential. Thanks to xet7.
- Run all tests at parallel or sequential. Thanks to xet7.
- Script to be executeable. Thanks to xet7.
- Update vscode sandbox. Thanks to xet7.
- Each run all tests now has logs at ../log/YYYY-MM-DD_HH-MM-SS/. Thanks to xet7.
and fixed the following bugs:
- Updated tests. Thanks to xet7.
- Fix parallel tests. Thanks to xet7.
- Fix tests. Thanks to xet7.
- Board Settings / Card Settings / Checklist item count (0/0) on minicard. Default: Off. Thanks to carl-unique and xet7.
- Card Settings popup options combined to same lines, and added more options. Thanks to xet7.
- Fix tests. Thanks to xet7.
- Playwright E2E: fixed three cross-process-contention flakes in the parallel run.
The Chromium / Firefox / WebKit browser jobs run as separate processes against ONE shared server + DB,
so specs that used fixed identifiers or global cleanups raced each other:
26-shared-templates.e2e.jsseeded a fixed email domain (usera@acme-e2e.invalid) in all three browsers, hittingE11000 duplicate keyon the uniqueemails.addressindex. Now uses a unique-per-run token for the org / team / domain and template titles.38-impersonation.e2e.jscleaned up with a globaldeleteMany({ reason: 'clickedImpersonate' })that deleted another browser’s in-flight audit record mid-poll. Scoped the cleanup to the test’s ownadminId.32-org-team-feature-toggles.e2e.jsexercisessetAllOrgsFeature/setAllTeamsFeature, which do a globalupdateMany({})across every org / team, so two browsers clobbered each other’s rows. These browser-agnostic server-method tests now run in a single project (Chromium); Firefox / WebKit skip them. Thanks to xet7.
- Server-side Mocha suite: fixed a startup crash and the 44 latent failures it had been hiding.
imports/i18n/i18n.test.jscrashed the whole run at load:chai6.x plugins (sinon-chai,chai-as-promised) are ESM-only, souse(require('sinon-chai'))handedchai.use()a module namespace instead of the plugin function (“fn is not a function”). Now imports the default export.- That unmasked a second load crash —
PositionHistory.helpers is not a function: themeteor testentry (server/lib/tests/index.js) never ran the.helpers/.attachSchemashim thatserver/main.jsbootstraps, so the first model to callCollection.helpers({...})threw. The shim is now imported first in the test entry. - With the suite finally running, 44 server tests failed because
meteor testonly loads what the specs import (not the app’s/server/imports). The specs now import the files that register the methods / globals under test (cards.vote/cards.pokerVote,api.attachment.*,cloneBoard,getBackgroundImageURL,applyListWidth,updateListSort,moveChecklist,userPositionHistory.*,archiveBoard,sendSMTPTestEmail, and theAttachmentsglobal). Also fixed genuine test bugs: thecards.vote/cards.pokerVotespecs were written synchronously against async methods; the header-login trust specs restored env vars withprocess.env.X = undefined(which stores the string"undefined"and shadowed the trusted-IP allowlist, making every trusted source read as untrusted); the DnsBleed decimal-loopback matcher assumed the integer host survived URL normalisation; and the dependencies-OpenAPI spec could not locate its source file from the built bundle. The twocards.archive/cards.movespecs tested Meteor methods that do not exist (archive / move are Minimongo document helpers secured byCards.allow/Cards.deny, already covered by thecards securitytests) and were removed. Server-side Mocha is now 409 passing, 0 failing. Thanks to xet7.
- Fix translations at Login and Register pages. Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.