Skip to content

v9.36

Choose a tag to compare

@github-actions github-actions released this 11 Jun 02:32
· 36 commits to main since this release

v9.36 2026-06-10 WeKan ® release

This release fixes the following CRITICAL SECURITY ISSUES of TokenBleed:

  • Fixed TokenBleed: unauthenticated login-token minting via un-awaited auth check in POST /api/createtoken/:userId
    (CWE-863, CWE-287). Authentication.checkUserId in server/authentication.js is an
    async function, so its 401 (undefined userId) and 403 (not an admin) throws become
    rejected promises rather than synchronous exceptions. The REST handlers in
    server/models/users.js and server/models/boards.js called it without await inside a
    plain synchronous try/catch, which cannot catch a rejected promise, so the failed check
    never stopped execution. POST /api/createtoken/:userId then went on to mint and return a
    usable login token for any user ID in the URL — including an admin — with no credentials at
    all (unauthenticated account takeover). The same detached-rejection bypass also affected
    GET /api/users, GET /api/users/:userId, PUT /api/users/:userId, POST /api/users/,
    DELETE /api/users/:userId, POST /api/deletetoken, GET /api/boards,
    GET /api/boards_count, DELETE /api/boards/:boardId, GET /api/users/:userId/boards and
    POST /api/boards/:boardId/copy. Fixed by awaiting every async Authentication check (and
    making the two non-async handlers async) so a failed check rejects before any privileged
    code runs. The same un-awaited pattern in the board/card/Excel/PDF export handlers
    (models/export.js, models/exportExcel.js, models/exportExcelCard.js, models/exportPDF.js,
    which were backstopped by exporter.canExport()) and in the checklist-create handler
    (server/models/checklists.js) was given the same await pass. Affected Wekan v9.35 and earlier.
    Thanks to Zion Boggan, xet7 and Claude.

and adds the following updates:

and fixes the following bugs:

  • Fix Admin Panel / Reports icons and spacing.
    Thanks to xet7.
  • Fix MultiSelect, so that it's possible to click select checkbox or minicard to select.
    Thanks to xet7 and Claude.
  • Fix LDAP_SYNC_ADMIN_GROUPS.
    so that admin status sync and group/role sync no longer require
    LDAP_GROUP_FILTER_ENABLE=true, which only controls the login restriction filter
    Thanks to xet7 and Claude.
  • Expand and fix REST API
    (Admin API, board member, card field, card copy/move).
    REST routes live in server/models/*.js (using WebApp.handlers.get/post/put/delete),
    their schemas in models/*.js, OpenAPI docs are generated by openapi/generate_openapi.py
    into public/api/wekan.yml, and api.py is the Python CLI wrapper.
    api.py already gained CLI wrappers for the endpoints that exist today:
    addboardmember, removeboardmember, setboardmemberrole, setcardmembers,
    setcardassignees, setcarddate, setcardlabels, movecard.
  • Issue #5998: Add/remove user to
    board with a role (BoardAdmin, Normal, etc).

    Already exists server-side: POST /api/boards/:boardId/members/:userId/add
    and POST /api/boards/:boardId/members/:userId/remove (action=add/remove), and
    POST /api/boards/:boardId/members/:memberId to change an existing member's
    permission flags (isAdmin, isCommentOnly, isWorker, isNoComments,
    isNormalAssignedOnly, isCommentAssignedOnly, isReadOnly,
    isReadAssignedOnly). api.py now wraps these with a friendly ROLE
    name → flags mapping.
    Implemented: the API itself now also accepts a single named role
    (admin/normal/commentOnly/worker/readOnly/normalAssignedOnly/
    commentAssignedOnly/readAssignedOnly/noComments), mapped to the permission
    booleans server-side by boardMemberRoleToFlags (server/lib/utils.js). When
    role is present it wins over the individual flags; an unknown role returns 400.
    Members are identified by userId only. Wired into both
    POST .../members/:userId/add and POST .../members/:memberId.
    Thanks to xet7 and Claude.
  • Add/Remove board member to card as card member or card assignee.
    Already exists via PUT /api/boards/:boardId/lists/:listId/cards/:cardId with
    members / assignees (array, or '' to clear). api.py now wraps these as
    setcardmembers / setcardassignees. These REPLACE the list.
    Implemented: merge-style endpoints to add/remove a single member or assignee
    (POST/DELETE /api/boards/:boardId/lists/:listId/cards/:cardId/members/:memberId
    and .../assignees/:assigneeId), using $addToSet/$pull so callers don't
    read-modify-write the whole array. Adding validates board membership
    (canAssignCardMember) and rejects non-members with 400; removing a stale id is
    allowed. Wrapped in api.py as addcardmember/removecardmember/
    addcardassignee/removecardassignee.
    Thanks to xet7 and Claude.
  • Issue #5897: Create Linked Card.
    Linked cards reference another card; the data model uses linkedId /
    type: 'cardType-linkedCard'. Implemented by reusing the existing
    POST /api/boards/:boardId/lists/:listId/cards route: when linkedId is present
    in the body the new card is created via Card.link (type cardType-linkedCard)
    instead of holding its own content. Linking across boards is allowed; the caller
    must have write access to the destination board and read access to the linked
    card's board (checkBoardAccess). Wrapped in api.py as linkcard.
    Thanks to xet7 and Claude.
  • Fix Issue #5846: Add/Remove card
    dates (Received/Start/Due/End).
    Setting dates already worked via PUT card with
    receivedAt, startAt, dueAt, endAt, but the handler only wrote a date when
    the value was truthy (if (req.body.receivedAt)), so an empty string could NOT
    clear a date — removing a date via the API did not work. Fixed in
    server/models/cards.js: whenever a date field is present in the request body, an
    empty string / null / "null" now $unsets the date and any other value sets
    it. api.py wraps this as setcarddate ... DATETYPE [DATEVALUE] (omit DATEVALUE,
    or pass an empty string, to clear).
    Thanks to xet7 and Claude.
  • Issue #5819: Bulk add/remove labels
    with API, and BoardAdmin label management in the right sidebar Labels
    hamburger/trigram menu.

    PUT card labelIds REPLACES the whole label set (wrapped as setcardlabels).
    Implemented: a multi-card bulk endpoint POST /api/boards/:boardId/cards/labels
    taking {cardIds, addLabelIds, removeLabelIds} that MERGES — existing labels are
    kept, removeLabelIds are dropped, addLabelIds are added, de-duplicated, across
    all listed cards in one request. addLabelIds are validated against the board's
    labels (request rejected with the offending ids otherwise); cards not on the board
    are reported in notFound. Wrapped in api.py as bulkcardlabels.
    Board label creation via PUT /api/boards/:boardId/labels is now gated to
    BoardAdmin (allowIsBoardAdmin); normal members can still apply existing labels
    to cards (including via the bulk endpoint). Still planned: the right-sidebar
    Labels hamburger/trigram menu BoardAdmin-only UI. Thanks to xet7 and Claude.
  • Fix Issue #5813: Card number is not
    unique when concurrently creating multiple cards via REST API; many cards got the
    same card number.
    Root cause: Board.getNextCardNumber() (models/boards.js)
    read the current max cardNumber and returned max + 1; two concurrent card
    creations read the same max and both got the same number — a classic
    read-then-increment race. Fixed by allocating card numbers from an atomic
    per-board counter (Counters.incrementCounterAsync('cardNumber-<boardId>') in
    models/counters.js), a single atomic findOneAndUpdate($inc) that is safe under
    concurrency. Existing boards (and imported boards with existing cards) are
    lazy-seeded on first use: the counter is initialized to the board's current
    max cardNumber via an idempotent $setOnInsert upsert, so no migration is
    needed and no number an existing card already has is reissued. Card numbers may
    have gaps (deleting a card does not decrement the counter), which is intended.
    Numbering stays per-board. The client keeps the old max + 1 read (card numbers are
    not authoritative there; the server insert recomputes). See also #4743 below.
    Thanks to xet7 and Claude.
  • Copy/Move Swimlane/List/Card to the same or a different board, before/after a
    position counted (number of swimlanes/lists/cards) from the top-left.

    Implemented: target position is a 0-based index counted from the top-left
    ("after N items"); the server converts it to a sort value between siblings via
    computeSortForIndex (server/lib/utils.js). New endpoints:
    POST /api/boards/:boardId/lists/:listId/cards/:cardId/copy,
    POST /api/boards/:boardId/swimlanes/:swimlaneId/copy and .../move,
    POST /api/boards/:boardId/lists/:listId/copy and .../move. Copy is a FULL
    deep copy via the existing model copy() methods (cards, checklists,
    attachments, custom-field values). Destination board write access is required
    (may differ from source). Wrapped in api.py as copycard,
    copyswimlane/moveswimlane, copylist/movelist. NOTE: List.move merges
    into an existing same-titled list on the destination board when one exists (a
    pre-existing model behavior); same-board list move is a pure reposition.
    Thanks to xet7 and Claude.
  • Issue #4815: API to get My Cards and
    Due Cards (also needs a user-scoped API).

    The web UI already has My Cards and Due Cards views. Implemented: a single
    GET /api/user/cards endpoint returning the current user's cards (where they are
    a member or assignee), with a ?due=true filter for cards that have a due date
    and an optional ?from=/?to= (ISO 8601) due-date range. Returns a compact
    minicard-like field set, sorted by due date. Wrapped in api.py as mycards.
    (Cross-user/admin querying, board filter and pagination were not requested for the
    first version; revisit if needed for users in very many boards.)
    Thanks to xet7 and Claude.
  • Issue #4811: After adding a card via
    the API, does the card count update properly?

    To verify: GET /api/boards/:boardId/cards_count and
    GET /api/boards/:boardId/lists/:listId/cards_count after POST card creation.
    Card counts are computed from the cards collection, so they should reflect
    API-created cards; this needs a regression test confirming counts update
    immediately after API create/delete (and are not stale due to caching).
  • Issue #3062: API for the Card
    Settings that live under Board Settings.

    Board Settings card defaults (which fields/badges show on cards and minicards) are
    the board's allows* toggles. Implemented: GET/PUT /api/boards/:boardId/cardSettings exposing those board-level allows* settings
    (read requires board access; write requires board write access). PUT accepts any
    subset of the recognized keys and ignores unknown ones. Per-user card presentation
    settings are intentionally out of scope. Wrapped in api.py as getcardsettings/
    setcardsetting.
    Thanks to xet7 and Claude.
  • Fix Issue #4743: Using the REST API
    to manipulate many cards crashes WeKan (100% CPU, server becomes
    unreachable/unusable, REST calls time out with HTTP 408).

    Reported workflow: every night delete all cards in a board and recreate them
    (create card → edit with custom fields/dates/labels → list all → delete all),
    done in a tight loop with no delay; WeKan eventually pegs CPU and stops
    responding. Same root area as #5813 (concurrent card creation), plus the cost of
    per-card cascade work (activities, server-side reactivity, custom-field defaults,
    before/after hooks) under bursts.
    Implemented so far: (1) atomic card numbering (see #5813) removes the hot
    read-modify-write contention; (2) bulk create/delete endpoints
    POST /api/boards/:boardId/lists/:listId/cards/bulk (body
    {authorId, swimlaneId, cards:[...]}) and
    DELETE /api/boards/:boardId/cards/bulk (body {cardIds:[...]}), each capped at
    500 items per request, so a sync job sends one request instead of hundreds —
    directly addressing the reporter's nightly delete-all/recreate workflow. Wrapped in
    api.py as bulkaddcards / bulkdeletecards. Bulk create returns a per-card
    result/error array; bulk delete returns {deleted, notFound} and only touches
    cards on the given board.
    Still planned/optional: per-token rate limiting and batched/optional activity
    logging were considered but not chosen for now; the bulk endpoints plus the atomic
    card numbering are expected to remove the hot contention. Revisit rate limiting if
    storms persist.
    Thanks to xet7 and Claude.
  • Fix REST API auth bugs found by the new E2E tests: awaited the membership/admin
    checks and return explicit status codes (CWE-862).
    Several auth helpers
    (checkAdminOrCondition, checkUserId) are async, so calling them without
    await let a denied caller's rejection slip past while the handler kept running:
    on card create that performed the write anyway (auth bypass), and on the board
    member endpoints the un-awaited rejection surfaced to the client as an HTTP 503.
    Fixes:
    • The pre-existing single-card create (POST .../cards) and the new bulk-create
      endpoint now await the membership check, so a logged-in non-board-member can
      no longer create cards.
    • The board member add/remove/permission endpoints
      (POST .../members/:userId/add, .../remove, POST .../members/:memberId)
      now require board admin (or site admin), awaited, returning a clean 401/403
      — instead of the previous un-awaited checkUserId (nominally site-admin-only,
      but effectively bypassed) that 503'd on denial.
    • Board-label creation (PUT .../labels) is gated to board admin / site admin,
      and it plus GET /api/user/cards return explicit 401/403 instead of letting an
      auth helper throw (which, under Express 4, would otherwise leave the request
      hanging). These were all found by the new REST API E2E test below. Thanks to
      xet7 and Claude.
  • End-to-end tests for the new REST API behavior and permissions. A new
    Playwright spec tests/playwright/specs/17-rest-api.e2e.js exercises the new/changed
    endpoints against a running server (real Bearer-token auth → real HTTP request →
    MongoDB change verified directly), asserting both correct data AND correct
    permissions: bulk create/delete (#4743), unique card numbers under bulk create
    (#5813), bulk label merge + rejection of off-board labels + BoardAdmin-gated label
    creation (#5819), add board member by named role + invalid-role rejection and
    card-member add validated against board membership (#5998), add/clear card date
    (#5846), linked-card creation (#5897), copy-card-to-position deep copy, board card
    settings GET/PUT (#3062), GET /api/user/cards with due filter (#4815), and that an
    unauthenticated request is denied. These use Playwright's HTTP request client (no
    browser is launched, so they run fast). The pure data/permission helpers they rely
    on live in server/lib/utils.js (boardMemberRoleToFlags, computeSortForIndex,
    mergeLabelIds, canAssignCardMember, isCardDateClear).
  • rebuild-wekan.sh test menu reorganized. There is now a menu option per test
    type so each can be run on its own: "Run ALL tests on http://localhost:3000 (start
    server, progress + summary)" (starts the server and runs import regression + Mocha +
    Node E2E + Playwright Chromium, streaming progress and printing a per-suite PASS/FAIL
    summary), plus standalone "Test Mocha unit + security + API-logic tests", "Test import
    regression", "Test Node E2E regressions", and the existing per-browser Playwright
    options. Playwright holds the browser-UI specs (01–16) and the API E2E spec (17);
    the fast unit/security/policy tests stay in the Mocha suite (meteor test --once --driver-package meteortesting:mocha).
    Thanks to xet7 and Claude.
  • Fix tests.
    Thanks to xet7 and Claude.

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