Wekan logo

Wekan

Open source kanban board application built with Meteor

Alternative to: trello


About Versions (100)

v9.32

2026-05-31

This release fixes the following CRITICAL SECURITY ISSUES:

  • Fix GHSA-hc3x-hq3m-663q: Server-Side Request Forgery (SSRF) via webhook integration URLs (CWE-918). Wekan’s outgoing-webhook integrations let a board admin store an arbitrary URL that is later fetched server-side, so a caller-controlled URL pointing at an internal address (cloud metadata at http://169.254.169.254/latest/meta-data/, loopback, RFC 1918 ranges, etc.) could be used to reach internal services or exfiltrate data.
    • Originally fixed at the delivery layer in v8.35 and v8.36 (the IntegrationBleed fixes). Fix IntegrationBleed (v8.35) routed webhook delivery in server/notifications/outgoing.js through fetchSafe (server/lib/ssrfGuard.js), which validates the URL and blocks private/loopback IPs, and Fix RebindBleed of IntegrationBleed (v8.36) hardened it further to resolve DNS once, pin the connection to the validated IP, and block redirects — closing the actual request-time SSRF including DNS rebinding.
    • New in this release: the missing input-side validation the advisory requested, added at the REST write paths in server/models/integrations.js. Those endpoints accepted webhook URLs without a robust check: POST /api/boards/:boardId/integrations relied only on the schema’s regex custom() validator (no DNS resolution, blind to a hostname that resolves to a private IP and to decimal/octal/IPv4-mapped-IPv6 encodings), and PUT /api/boards/:boardId/integrations/:intId wrote the URL via Integrations.direct.updateAsync, which bypasses schema validation entirely, so updated URLs were never validated at the data layer. Both endpoints now run the DNS-aware validateAttachmentUrl() (models/lib/attachmentUrlValidation.js, the same validator already used for attachment imports) before storing the URL, rejecting private/loopback/link-local/reserved targets with HTTP 400. Together with the v8.35/v8.36 fetchSafe delivery guard and the schema validator on client-side inserts/updates, webhook URLs are now validated on every write path and again at delivery. Thanks to Claude.
  • Fix GHSA-7w2h-g83c-jqrp: Authorization bypass in copyBoard DDP method allows any user to copy private boards (CWE-862). The copyBoard Meteor method in server/publications/boards.js had no authorization check: any logged-in user could copy any board by ID — including private boards they are not a member of — cloning all cards, checklists, custom fields, labels and rules, while the equivalent REST endpoint POST /api/boards/:boardId/copy correctly required board admin. The method also looped over caller-supplied properties (for (const key in properties) board[key] = properties[key]) and copied them onto the new board, letting an attacker inject arbitrary fields such as members (to add themselves as admin of the copy) or permission: 'public' (to expose the copy to everyone). The member-level fix shipped in v9.09 as part of the AuthBleed fixes (caller must be authenticated, board must exist, caller must be a board member, and members/permission are stripped from properties before the copy). This release tightens it further to full parity with the REST endpoint: copyBoard now requires the caller to be a board admin (board.hasAdmin(this.userId), the same {isActive:true, isAdmin:true} condition the REST API enforces via checkAdminOrCondition) rather than merely a member.
  • Fix GHSA-cv95-8h7c-2ffq: Missing authorization on OIDC Meteor methods allows privilege escalation to admin (CWE-269, CWE-862). Six Meteor methods used internally by the OIDC login flow were registered as globally DDP-callable with no authorization, while their non-OIDC counterparts require admin. setCreateOrgFromOidc and setOrgAllFieldsFromOidc (server/models/org.js), setCreateTeamFromOidc and setTeamAllFieldsFromOidc (server/models/team.js) let any authenticated user create/rename/ deactivate/modify arbitrary organizations and teams — including orgAutoAddUsersWithDomainName — bypassing the admin-only restriction. Most critically, groupRoutineOnLogin (packages/wekan-oidc/oidc_server.js) sets isAdmin from caller-supplied group data, so with PROPAGATE_OIDC_DATA enabled any authenticated user could call it over DDP with {groups:[{isAdmin:true,forceCreate:true}]} and promote themselves to global admin; boardRoutineOnLogin could likewise add the caller to the default board. These six methods are only ever invoked server-side (via Meteor.callAsync) during the OIDC handshake, where a fix that checks isAdmin/this.userId would break legitimate group/admin propagation (the user is not yet logged in or admin at that point). Fixed by rejecting any direct client/DDP invocation: a server-to-server Meteor.callAsync runs with this.connection === null, whereas a client call has a non-null connection, so each of the six methods now throws not-authorized when this.connection !== null. The legitimate OIDC login flow is unaffected. Thanks to alexwaira for the coordinated disclosure, and Claude.
  • Fix GHSA-mp7g-hj5q-gxhq: OIDC Account Takeover via Unconditional Email-Based Account Merge in Accounts.onCreateUser hook (CWE-287). The onCreateUser hook in server/models/users.js unconditionally merged an incoming OIDC login into any existing Wekan account whose email or username matched the (attacker-controlled) OIDC claims — no ownership check, no email-verification check, no notification. An attacker who could present a matching email/username claim (trivial on self-hosted Keycloak/Authentik, where the email/email_verified claims are attacker-settable) inherited the victim’s _id, boards, cards, attachments, API tokens and admin status, all during the attacker’s own first OIDC login with no victim interaction. Fixed to fail closed, mirroring LDAP_MERGE_EXISTING_USERS: matching is now by email only (never username); auto-linking is opt-in via the new OAUTH2_MERGE_EXISTING_USERS setting (OFF by default, so the default deployment never merges); even when enabled the OIDC provider must assert email_verified=true; otherwise the OIDC login is rejected with oidc-email-already-in-use instead of merging. The provider’s email_verified claim is now captured into the OIDC service data in packages/wekan-oidc/oidc_server.js. Thanks to alexwaira for the coordinated disclosure, and Claude.
  • Fix GHSA-6733-4wgq-8xvr: Read-only board members could create/modify/delete Custom Fields. (privilege escalation via read-level authz on write operations, CWE-862). All six mutating REST handlers in server/models/customFields.js (POST/PUT custom-fields, POST/PUT/DELETE dropdown-items, DELETE custom-fields) called the read-level Authentication.checkBoardAccess instead of the write-level checkBoardWriteAccess, letting a board member with the read-only role (isReadOnly / isReadAssignedOnly) write Custom Field data via the REST API when WITH_API=true. Replaced the check with checkBoardWriteAccess in all six mutating handlers (the two GET handlers correctly stay on checkBoardAccess), mirroring lists.js/swimlanes.js/cards.js. Thanks to Wernerina for the coordinated disclosure, and Claude.
  • Fix regression from the avatar RCE fix GHSA-35j7-h385-2q9g: external antivirus scanner broken (asyncExec undefined). The avatar RCE fix renamed asyncExec to asyncExecFile in models/fileValidation.js, but the admin-configured external scanner command line still called the now-undefined asyncExec, throwing ReferenceError (swallowed by the catch) and making every upload silently fail validation whenever an external scanner was configured. Restored a shell-based asyncExec used only for that admin-configured command line; MIME detection still uses the shell-free asyncExecFile. Thanks to Claude.
  • Fix CodeQL 68: Polynomial regex DoS in Jade parser. Thanks to CodeQL and Claude.
  • Fix CodeQL 69: Polynomial regex DoS in Jade parser, part 2. Remove the redundant $ anchor from the interpolation regex in compiler.js and bundled jade.js. The greedy [\s\S]* already matches to end of string, so dropping $ keeps the same match while eliminating the backtracking that CodeQL alert js/polynomial-redos flagged. Thanks to CodeQL and Claude.
  • Fix CodeQL 63: Incomplete string escaping or encoding in bundled jade.js. In uglify-js’s make_string (bundled twice into jade’s browser bundle), the chosen quote was escaped in a trailing str.replace(/'/g, "\\'") that CodeQL alert js/incomplete-sanitization flags for not escaping backslashes in that same call. Backslashes were already escaped in the earlier single-pass replace, so this was a local false positive, but the fix folds the quote escaping into that same pass (escaping both quotes), making the escaping atomic and complete while keeping the emitted string literals decode-equivalent. Thanks to CodeQL and Claude.
  • Fix CodeQL 67: Incomplete multi-character sanitization. Thanks to CodeQL and GitHub Copilot.
  • Fix CodeQL 60: Useless regular-expression character escape. Thanks to CodeQL and GitHub Copilot.
  • Fix CodeQL 58: Incomplete multi-character sanitization. Thanks to CodeQL and GitHub Copilot.
  • Fix CodeQL 57: Incomplete multi-character sanitization. Thanks to CodeQL and GitHub Copilot.
  • Fix CodeQL 56: Incomplete string escaping or encoding](https://github.com/wekan/wekan/commit/c5e42607af5a0a396c0c85dba3652b8be28a2ff3). Thanks to CodeQL and GitHub Copilot.
  • Fix CodeQL 55: Incomplete string escaping or encoding. Thanks to CodeQL and GitHub Copilot.
  • Fix CodeQL 48: Clear-text logging of sensitive information. Thanks to CodeQL and GitHub Copilot.
  • Fix CodeQL 418: Clear-text logging of sensitive information. Thanks to CodeQL and GitHub Copilot.
  • Fix CodeQL 419: Clear-text logging of sensitive information. Thanks to CodeQL and GitHub Copilot.
  • Delete calendar demos, so that CodeQL stops complaining. Thanks to xet7.
  • Fix CodeQL 35: Insecure randomness. Thanks to CodeQL and GitHub Copilot.
  • Fix CodeQL 417: Workflow does not contain permissions. Thanks to CodeQL and GitHub Copilot.

and adds the following updates:

and adds the following new features:

  • Add User board access roles to Admin Panel / People / Roles. A new global “Roles” tab in Admin Panel / People lets a site admin choose which board roles are allowed to invite users to a board (“Allow Invite to Board”). Each board role has its own toggle — Board Admin, Normal, Worker, Comment only, No comments, Only Assigned Normal, Only Assigned Comment, Read Only and Only Assigned Read — plus an “All Board Members” master toggle that selects them all. The policy is enforced server-side in the inviteUserToBoard and searchUsers methods, and the add-member button in the board sidebar is shown only to roles the policy allows. Global Admin Panel users (site admins) always have all rights and cannot be restricted here; this is kept clearly distinct in code from the per-board “Board Admin” role. Secure default: only Board Admin and Normal may invite. Thanks to xet7.

and fixes the following bugs:

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