Compare commits

...
6 Commits
Author SHA1 Message Date
github-actions[bot] 02cf8fdf16 Bump versions for v9.64 2026-06-20 01:20:53 +00:00
Lauri Ojansivu b0910e48d4 Updated ChangeLog. 2026-06-20 04:16:59 +03:00
Lauri OjansivuandClaude Opus 4.8 4afd8b8296 Updated ChangeLog: ChecklistBleed security fix + docker guard fix (upcoming)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 04:12:53 +03:00
Lauri OjansivuandClaude Opus 4.8 b1ca76007b Fix GHSA-gv8h-5p3p-6hx7 (ChecklistBleed): checklist cross-board write via DDP allow rule (CWE-863)
The Checklists and ChecklistItems collection allow rules authorize an
update only against the document's CURRENT (source) cardId. A DDP client
with write access to its own board could move a checklist/item it owns
onto a card in a private board it is not a member of by $set-ting a new
cardId (and, for items, checklistId); the before.update hook then
denormalizes boardId from the destination card, attaching the
attacker-controlled document to the victim's private board. Same class
as denyCrossBoardMove / BoardBleed (GHSA-gm7v-pc38-53jr), but for the
card-attached checklist documents that the boardId-only deny did not
cover.

Add denyCrossBoardMoveByCard and denyCrossBoardMoveByChecklistItem in
server/lib/utils.js, which reject any update whose destination board
(resolved from a new boardId, cardId, or checklistId in the modifier)
the caller cannot write to, and wire them into Checklists.deny /
ChecklistItems.deny. Legitimate moves into writable boards and same-card
edits are unaffected; the server-side moveChecklist Meteor method bypasses
allow/deny and is unchanged. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 04:10:36 +03:00
github-actions[bot] 591a7a458b Bump versions for v9.63 2026-06-20 01:07:24 +00:00
Lauri OjansivuandClaude Opus 4.8 069dbc11f4 Fix docker pre-build version guard false-failing the release
The bundle-version guard added for the Admin-Panel-version fix assumed
the WeKan app package.json ships as a standalone file with a v-prefixed
version, and made 'not found' fatal. Meteor does not ship it that way:
it inlines the app package.json into the compiled
bundle/programs/server/app/app.js as a JSON module
({"name":"wekan","version":"v9.63.0",...}), so the guard found no
v-prefixed package.json and aborted the v9.63 docker build (exit 1).

Read the version from the inlined module in app.js instead (anchored on
"name":"wekan", exactly what require('/package.json') resolves to), and
make a detection miss a WARNING that continues rather than a hard
failure — only a confirmed version MISMATCH blocks the release.
--build-arg VERSION remains the actual guarantee.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 04:01:59 +03:00
15 changed files with 274 additions and 37 deletions

No files matched your search

+34 -18
View File
@@ -714,33 +714,49 @@ jobs:
# Belt-and-suspenders against the "image tagged vX shows vY in the Admin
# Panel" bug: the image is just the unzipped wekan-<version>-<arch>.zip
# bundle (Dockerfile moves it to /build), and the Admin Panel shows that
# bundle's app package.json version (server/statistics.js reads
# require('/package.json').version). Assert the amd64 bundle's WeKan
# version equals the release version BEFORE the expensive multi-arch
# buildx, so a stale/mismatched bundle fails fast with a clear message
# instead of shipping (and pushing to three registries) a mislabeled image.
# bundle's WeKan version (server/statistics.js reads
# require('/package.json').version). Check the amd64 bundle's WeKan version
# equals the release version BEFORE the expensive multi-arch buildx, so a
# stale/mismatched bundle fails fast instead of shipping (and pushing to
# three registries) a mislabeled image.
#
# Meteor does NOT ship the app package.json as a standalone file — it
# inlines it into the compiled bundle/programs/server/app/app.js as a JSON
# module, e.g. {"name":"wekan","version":"v9.63.0",...}. That inlined
# module (anchored on "name":"wekan") is exactly what require('/package.json')
# resolves to at runtime, so it is the authoritative thing to check.
#
# IMPORTANT: only a CONFIRMED mismatch fails the release. If the version
# cannot be located (bundle layout changed, app.js minified differently),
# warn and continue — inability to verify must not block a good release,
# since --build-arg VERSION below is the actual guarantee. (An earlier
# version of this guard grepped for a v-prefixed *package.json* and made
# "not found" fatal, which false-failed the whole v9.63 docker build.)
run: |
set -euo pipefail
url="https://github.com/wekan/wekan/releases/download/v${VERSION}/wekan-${VERSION}-amd64.zip"
wget -q "$url" -O bundle.zip
tmp="$(mktemp -d)"
# Extract only package.json files (not the whole multi-hundred-MB bundle).
unzip -o -q bundle.zip '*package.json' -d "$tmp"
# The WeKan app package.json is the only one whose "version" carries a
# leading 'v' (e.g. "v9.61.0"); every dependency uses plain semver. So a
# v-prefixed "version" uniquely identifies the app's own package.json.
found="$(grep -rhoE '"version": "v[0-9][^"]*"' "$tmp" \
| grep -oE 'v[0-9][^"]*' | sort -u | head -1 || true)"
# Extract only the compiled app program (not the whole multi-hundred-MB
# bundle); the inlined app package.json lives in app/app.js.
unzip -o -q bundle.zip 'bundle/programs/server/app/app.js' -d "$tmp" || true
appjs="$tmp/bundle/programs/server/app/app.js"
found=""
if [ -f "$appjs" ]; then
match="$(grep -aoE '"name":"wekan","version":"v?[0-9][^"]*"' "$appjs" | head -1 || true)"
found="$(printf '%s' "$match" | sed -E 's/.*"version":"([^"]*)".*/\1/')"
fi
rm -rf "$tmp" bundle.zip
if [ -z "$found" ]; then
echo "::error::Could not find a v-prefixed WeKan version in the bundle's package.json files."
echo "::warning::Could not locate the WeKan version in the bundle (app.js layout may have changed); skipping the bundle-version check. --build-arg VERSION still guarantees the correct bundle."
exit 0
fi
norm="${found#v}"
if [ "$norm" != "${VERSION}.0" ] && [ "$norm" != "${VERSION}" ]; then
echo "::error::Bundle reports app version ${found} but the release is v${VERSION}. The bundle baked into the image is stale/mismatched — the Admin Panel would show ${norm}. Aborting before build."
exit 1
fi
if [ "$found" != "v${VERSION}.0" ] && [ "$found" != "v${VERSION}" ]; then
echo "::error::Bundle reports app version $found but the release is v${VERSION}. The bundle baked into the image is stale/mismatched — the Admin Panel would show $found. Aborting before build."
exit 1
fi
echo "Bundle app version $found matches release v${VERSION}."
echo "Bundle app version ${found} matches release v${VERSION}."
- name: Build and push multi-arch image
# Pass VERSION explicitly as a build-arg. The Dockerfile's `ARG VERSION`
+49
View File
@@ -26,6 +26,55 @@ Versions:
- WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible,
it was fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
# v9.64 2026-06-20 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUE of [ChecklistBleed](https://wekan.fi/hall-of-fame/checklistbleed/):
- [Fixed ChecklistBleed: any authenticated user can write checklist data into a private board they are not a member of (cross-board write via collection allow rule)](https://github.com/wekan/wekan/commit/b1ca76007)
([GHSA-gv8h-5p3p-6hx7](https://github.com/wekan/wekan/security/advisories/GHSA-gv8h-5p3p-6hx7),
CWE-863 Incorrect Authorization). This is the same class as [BoardBleed](https://wekan.fi/hall-of-fame/boardbleed/)
(GHSA-gm7v-pc38-53jr), but for the card-attached Checklist and ChecklistItem documents that the
`boardId`-only `denyCrossBoardMove` fix did not cover. Checklists and checklist items are attached
to a card and carry a denormalized `boardId`; they are MOVED between cards by `$set`-ting a new
`cardId` (and, for items, a new `checklistId`) in a direct DDP collection update, after which the
`Checklists.before.update` hook re-derives `boardId` from the destination card. The collection
allow rules (`server/permissions/checklists.js`, `server/permissions/checklistItems.js`)
authorized an update by checking only the document's CURRENT (source) `cardId` — i.e. the
attacker's own board — and never inspected the new destination `cardId`/`checklistId`/`boardId`.
Because every logged-in user can create a board where they are a write-capable member, a
low-privileged user with write access to one board could create a checklist/item on their own
card and then, in a single `/checklists/update` or `/checklistItems/update` DDP call, set its
`cardId` to a card in a private board where they are not a member (if they know the target card
id): the allow rule saw the attacker's source card, approved the write, and the before-update hook
attached the attacker-controlled document to the victim's private board. The protected
`moveChecklist` Meteor method correctly checks both source and destination board membership, but a
DDP client can bypass that method and update the collections directly. CVSS:3.1 Moderate
(AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N). Fixed by adding `denyCrossBoardMoveByCard` and
`denyCrossBoardMoveByChecklistItem` helpers in `server/lib/utils.js` and a `Checklists.deny`/
`ChecklistItems.deny` `update` rule on each collection that rejects any update whose destination
board — resolved from a new `boardId`, `cardId`, or `checklistId` in the modifier — the caller has
no write access to. Legitimate moves into boards the user belongs to and same-card edits keep
working, and the server-side `moveChecklist` method (which bypasses allow/deny) is unchanged.
A regression test (`server/lib/tests/checklistbleed.security.tests.js`) was added.
Affected Wekan v9.62 and earlier.
Thanks to DavidCarliez, xet7 and Claude.
and adds the following updates:
- [Fix the Docker pre-build version guard false-failing the release](https://github.com/wekan/wekan/commit/069dbc11f).
The bundle-version guard added for the Admin-Panel-version fix assumed the WeKan app
`package.json` ships as a standalone file with a `v`-prefixed version, and made "not found"
fatal. Meteor does not ship it that way — it inlines the app `package.json` into the compiled
`bundle/programs/server/app/app.js` as a JSON module (`{"name":"wekan","version":"v9.63.0",...}`),
so the guard found no v-prefixed `package.json` and aborted the v9.63 Docker build. The guard now
reads the version from that inlined module (anchored on `"name":"wekan"`, exactly what
`require('/package.json')` resolves to), and a detection miss is now a warning that continues
rather than a hard failure — only a confirmed version MISMATCH blocks the release, since
`--build-arg VERSION` is the actual guarantee.
Thanks to xet7 and Claude.
Thanks to above GitHub users for their contributions and translators for their translations.
# v9.63 2026-06-19 WeKan ® release
This release adds the following features:
+1 -1
View File
@@ -7,7 +7,7 @@ LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=9.63
ARG VERSION=9.64
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v9.63.0"
appVersion: "v9.64.0"
files:
userUploads:
- README.md
+2 -2
View File
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-9.63-win64.zip](https://github.com/wekan/wekan/releases/download/v9.63/wekan-9.63-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
1. [wekan-9.64-win64.zip](https://github.com/wekan/wekan/releases/download/v9.64/wekan-9.64-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
2. [node.exe](https://nodejs.org/dist/latest-v24.x/win-x64/node.exe)
@@ -22,7 +22,7 @@ Right click and download files 1-4:
6. Double click `mongodb-windows-x86_64-7.0.34-signed.msi` . In installer, uncheck downloading MongoDB compass.
7. Unzip `wekan-9.63-win64.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-9.64-win64.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "wekan",
"version": "v9.63.0",
"version": "v9.64.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wekan",
"version": "v9.63.0",
"version": "v9.64.0",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-s3": "^3.1071.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wekan",
"version": "v9.63.0",
"version": "v9.64.0",
"description": "Open-Source kanban",
"private": true,
"repository": {
+3 -3
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf8" />
<title>WeKan REST API v9.63</title>
<title>WeKan REST API v9.64</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@@ -423,7 +423,7 @@ data-styled.g139[id="sc-iIvZUO"]{content:"kAmnlO,"}/*!sc*/
55.627 l 55.6165,55.627 -231.245496,231.24803 c -127.185,127.1864
-231.5279,231.248 -231.873,231.248 -0.3451,0 -104.688,
-104.0616 -231.873,-231.248 z
" fill="currentColor"></path></g></svg></div></div><div class="sc-jtyQFi ldXCvc api-content"><div class="sc-eCQgVK jleRXN"><div class="sc-iCECmn lhRBUS"><div class="sc-hKVpXn kktglN api-info"><h1 class="sc-fuztkK sc-ctiVdb eRMVDe cBgvIi">Wekan REST API<!-- --> <span>(<!-- -->v9.63<!-- -->)</span></h1><p>Download OpenAPI specification<!-- -->:</p><div class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV"></div><div data-role="redoc-summary" html="" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV"></div><div data-role="redoc-description" html="&lt;p&gt;The REST API allows you to control and extend Wekan with ease.&lt;/p&gt;
" fill="currentColor"></path></g></svg></div></div><div class="sc-jtyQFi ldXCvc api-content"><div class="sc-eCQgVK jleRXN"><div class="sc-iCECmn lhRBUS"><div class="sc-hKVpXn kktglN api-info"><h1 class="sc-fuztkK sc-ctiVdb eRMVDe cBgvIi">Wekan REST API<!-- --> <span>(<!-- -->v9.64<!-- -->)</span></h1><p>Download OpenAPI specification<!-- -->:</p><div class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV"></div><div data-role="redoc-summary" html="" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV"></div><div data-role="redoc-description" html="&lt;p&gt;The REST API allows you to control and extend Wekan with ease.&lt;/p&gt;
&lt;p&gt;If you are an end-user and not a dev or a tester, &lt;a href=&quot;https://github.com/wekan/wekan/issues/new&quot;&gt;create an issue&lt;/a&gt; to request new APIs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;All API calls in the documentation are made using &lt;code&gt;curl&lt;/code&gt;. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.&lt;/p&gt;
@@ -1965,7 +1965,7 @@ comment-only / worker members are rejected).</p>
" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG"><p>Attachment not found</p>
</div></button></div></div></div><div class="sc-jSppWd sc-gKkgUA fpMlmc bdQQyo"><div class="sc-fXwuWv fYxpnv"><button class="sc-jWMFtl jzaJhV"><span type="delete" class="sc-eEFuoE iINwyG http-verb delete">delete</span><span class="sc-FpjRO dsIRaZ">/api/attachment/delete/{attachmentId}</span><svg class="sc-dYjPD bQKUih" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></button><div aria-hidden="true" class="sc-fmtEmb tUxhh"><div class="sc-ljIcGq bAnPPh"><div html="" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv"></div><div tabindex="0" role="button"><div class="sc-jlJOIR PEJyb"><span></span>/api/attachment/delete/{attachmentId}</div></div></div></div></div></div></div></div></div><div class="sc-ekVkVN bjzTxL"></div></div></div>
<script>
const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","info":{"title":"Wekan REST API","version":"v9.63","description":"The REST API allows you to control and extend Wekan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production Wekan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}},"/api/admin/attachment-settings":{"get":{"operationId":"get_attachment_settings","summary":"Get the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. Returns the Admin Panel >\n Attachments settings document, including `limitSettings.attachmentsUploadBlocked`\n and `limitSettings.avatarsUploadBlocked` plus the size limits. Cloud-storage\n secret keys (S3 secretAccessKey, Azure accountKey/connectionString, GCS\n credentials) are masked and never returned; a boolean `<field>Set` marker\n shows whether a value exists.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}},"put":{"operationId":"update_attachment_settings","summary":"Update the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. The request body is a\n partial settings object (for example\n `{\"limitSettings\": {\"avatarsUploadBlocked\": true}}`). It is applied through\n the same update path as the `updateAttachmentStorageSettings` Meteor method,\n so validation and `$set` semantics are identical: cloud-storage secrets left\n blank are preserved. The masked, updated settings document is returned.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}}},"/api/admin/domains":{"get":{"operationId":"get_admin_domains","summary":"List email domains with their user counts (GlobalAdmin)","description":"Only the gLine truncated
const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","info":{"title":"Wekan REST API","version":"v9.64","description":"The REST API allows you to control and extend Wekan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production Wekan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}},"/api/admin/attachment-settings":{"get":{"operationId":"get_attachment_settings","summary":"Get the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. Returns the Admin Panel >\n Attachments settings document, including `limitSettings.attachmentsUploadBlocked`\n and `limitSettings.avatarsUploadBlocked` plus the size limits. Cloud-storage\n secret keys (S3 secretAccessKey, Azure accountKey/connectionString, GCS\n credentials) are masked and never returned; a boolean `<field>Set` marker\n shows whether a value exists.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}},"put":{"operationId":"update_attachment_settings","summary":"Update the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. The request body is a\n partial settings object (for example\n `{\"limitSettings\": {\"avatarsUploadBlocked\": true}}`). It is applied through\n the same update path as the `updateAttachmentStorageSettings` Meteor method,\n so validation and `$set` semantics are identical: cloud-storage secrets left\n blank are preserved. The masked, updated settings document is returned.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}}},"/api/admin/domains":{"get":{"operationId":"get_admin_domains","summary":"List email domains with their user counts (GlobalAdmin)","description":"Only the gLine truncated
var container = document.getElementById('redoc');
Redoc.hydrate(__redoc_state, container);
+1 -1
View File
@@ -1,7 +1,7 @@
swagger: '2.0'
info:
title: Wekan REST API
version: v9.63
version: v9.64
description: |
The REST API allows you to control and extend Wekan with ease.
+2 -2
View File
@@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
appTitle = (defaultText = "Wekan"),
# The name of the app as it is displayed to the user.
appVersion = 963,
appVersion = 964,
# Increment this for every release.
appMarketingVersion = (defaultText = "9.63~2026-06-19"),
appMarketingVersion = (defaultText = "9.64~2026-06-20"),
# Human-readable presentation of the app version.
minUpgradableAppVersion = 0,
@@ -0,0 +1,93 @@
/* eslint-env mocha */
import { expect } from 'chai';
import sinon from 'sinon';
import Boards from '/models/boards';
import Cards from '/models/cards';
import Checklists from '/models/checklists';
import {
denyCrossBoardMoveByCard,
denyCrossBoardMoveByChecklistItem,
} from '/server/lib/utils';
// Security regression test for GHSA-gv8h-5p3p-6hx7 (ChecklistBleed):
// a DDP client must not be able to move a Checklist / ChecklistItem onto a card
// in a private board it cannot write to, by $set-ting a new cardId / checklistId
// (boardId is then denormalized from the destination card). The deny helpers must
// return true (DENY) for such cross-board moves and false (ALLOW) for legitimate
// same-board edits and moves into boards the caller can write to.
describe('ChecklistBleed — checklist/item cross-board move deny rules (GHSA-gv8h-5p3p-6hx7)', function() {
const ATTACKER = 'attacker';
// Board the attacker is a write-capable member of.
const sourceBoard = {
members: [{ userId: ATTACKER, isActive: true, isNoComments: false, isCommentOnly: false, isWorker: false, isReadOnly: false, isReadAssignedOnly: false }],
};
// Private board the attacker is NOT a member of.
const victimBoard = {
members: [{ userId: 'victim', isActive: true, isNoComments: false, isCommentOnly: false, isWorker: false, isReadOnly: false, isReadAssignedOnly: false }],
};
const boards = { sourceBoard, victimBoard };
const cards = {
attackerCard: { _id: 'attackerCard', boardId: 'sourceBoard' },
victimCard: { _id: 'victimCard', boardId: 'victimBoard' },
};
const checklists = {
victimChecklist: { _id: 'victimChecklist', cardId: 'victimCard' },
attackerChecklist: { _id: 'attackerChecklist', cardId: 'attackerCard' },
};
beforeEach(function() {
sinon.stub(Boards, 'findOneAsync').callsFake(async id => boards[id] || null);
sinon.stub(Cards, 'findOneAsync').callsFake(async id => cards[id] || null);
sinon.stub(Checklists, 'findOneAsync').callsFake(async id => checklists[id] || null);
});
afterEach(function() {
sinon.restore();
});
describe('denyCrossBoardMoveByCard (Checklists)', function() {
it('DENIES moving a checklist to a card in a board the caller cannot write to', async function() {
const denied = await denyCrossBoardMoveByCard(ATTACKER, { $set: { cardId: 'victimCard' } });
expect(denied).to.equal(true);
});
it('DENIES a direct boardId change to an unwritable board', async function() {
const denied = await denyCrossBoardMoveByCard(ATTACKER, { $set: { boardId: 'victimBoard' } });
expect(denied).to.equal(true);
});
it('DENIES a move to an unknown (non-existent) card id (fail closed)', async function() {
const denied = await denyCrossBoardMoveByCard(ATTACKER, { $set: { cardId: 'doesNotExist' } });
expect(denied).to.equal(true);
});
it('ALLOWS a same-board move to a card the caller can write to', async function() {
const denied = await denyCrossBoardMoveByCard(ATTACKER, { $set: { cardId: 'attackerCard' } });
expect(denied).to.equal(false);
});
it('ALLOWS an unrelated edit that does not move the document', async function() {
const denied = await denyCrossBoardMoveByCard(ATTACKER, { $set: { title: 'renamed' } });
expect(denied).to.equal(false);
});
});
describe('denyCrossBoardMoveByChecklistItem (ChecklistItems)', function() {
it('DENIES re-parenting an item to a checklist on an unwritable board', async function() {
const denied = await denyCrossBoardMoveByChecklistItem(ATTACKER, { $set: { checklistId: 'victimChecklist' } });
expect(denied).to.equal(true);
});
it('DENIES moving an item to a card in an unwritable board', async function() {
const denied = await denyCrossBoardMoveByChecklistItem(ATTACKER, { $set: { cardId: 'victimCard' } });
expect(denied).to.equal(true);
});
it('ALLOWS re-parenting to a checklist on a board the caller can write to', async function() {
const denied = await denyCrossBoardMoveByChecklistItem(ATTACKER, { $set: { checklistId: 'attackerChecklist' } });
expect(denied).to.equal(false);
});
});
});
+55
View File
@@ -1,4 +1,6 @@
import Boards from '/models/boards';
import Cards from '/models/cards';
import Checklists from '/models/checklists';
export function allowIsBoardAdmin(userId, board) {
return board && board.hasAdmin(userId);
@@ -53,6 +55,59 @@ export async function denyCrossBoardMove(userId, modifier) {
return !allowIsBoardMemberWithWriteAccess(userId, await Boards.findOneAsync(newBoardId));
}
// Shared destination gate for the checklist deny rules below: returns true to
// DENY when boardId names a board the caller cannot write to. An unknown/missing
// board id also denies, matching denyCrossBoardMove's fail-closed behavior.
async function denyMoveToUnwritableBoard(userId, boardId) {
if (typeof boardId !== 'string' || !boardId) return false;
return !allowIsBoardMemberWithWriteAccess(userId, await Boards.findOneAsync(boardId));
}
// Security (GHSA-gv8h-5p3p-6hx7): Checklists and ChecklistItems are attached to a
// card and carry a denormalized boardId. They are MOVED between cards by setting a
// new cardId (and, for items, a new checklistId) in a direct collection update; a
// before.update hook then re-derives boardId from the destination card. The allow
// rules only authorize against the document's CURRENT (source) cardId, so a DDP
// client with write access to its own board can relocate a checklist/item it owns
// onto a card in a private board it is not a member of. This is the same class as
// denyCrossBoardMove / BoardBleed, but for the card-attached checklist documents
// that the boardId-only deny rule does not cover.
//
// Returns true to DENY any update whose destination board — resolved from a new
// boardId or a new cardId in the modifier — the caller cannot write to.
// Legitimate moves (destination writable) and same-card edits are unaffected, and
// server-side Meteor methods bypass allow/deny entirely, so the trusted
// moveChecklist method still works.
export async function denyCrossBoardMoveByCard(userId, modifier) {
const set = modifier && modifier.$set;
if (!set) return false;
// Direct boardId change (defense in depth, same as denyCrossBoardMove).
if (await denyMoveToUnwritableBoard(userId, set.boardId)) return true;
// cardId change: resolve the destination card's board.
if (typeof set.cardId === 'string' && set.cardId) {
const card = await Cards.findOneAsync(set.cardId);
if (!card) return true;
if (await denyMoveToUnwritableBoard(userId, card.boardId)) return true;
}
return false;
}
// As denyCrossBoardMoveByCard, plus the ChecklistItem-only checklistId move: an
// item can be re-parented to another checklist, which (via that checklist's card)
// may live on a board the caller cannot write to. Returns true to DENY.
export async function denyCrossBoardMoveByChecklistItem(userId, modifier) {
if (await denyCrossBoardMoveByCard(userId, modifier)) return true;
const set = modifier && modifier.$set;
if (set && typeof set.checklistId === 'string' && set.checklistId) {
const checklist = await Checklists.findOneAsync(set.checklistId);
if (!checklist) return true;
const card = await Cards.findOneAsync(checklist.cardId);
if (!card) return true;
if (await denyMoveToUnwritableBoard(userId, card.boardId)) return true;
}
return false;
}
// Check if user has write access via a card's board
export async function allowIsBoardMemberWithWriteAccessByCard(userId, card) {
const board = card && await Boards.findOneAsync(card.boardId);
+13 -1
View File
@@ -1,6 +1,6 @@
import Cards from '/models/cards';
import ChecklistItems from '/models/checklistItems';
import { allowIsBoardMemberWithWriteAccessByCard } from '/server/lib/utils';
import { allowIsBoardMemberWithWriteAccessByCard, denyCrossBoardMoveByChecklistItem } from '/server/lib/utils';
ChecklistItems.allow({
async insert(userId, doc) {
@@ -17,3 +17,15 @@ ChecklistItems.allow({
},
fetch: ['userId', 'cardId'],
});
// Security (GHSA-gv8h-5p3p-6hx7): the allow rule above only checks write access
// on the item's SOURCE card/board, so a DDP client could move a checklist item
// into a private board it is not a member of by $set-ting a new cardId or
// checklistId (boardId is then denormalized from the destination card). Deny any
// move whose destination board the caller cannot write to.
ChecklistItems.deny({
async update(userId, doc, fieldNames, modifier) {
return await denyCrossBoardMoveByChecklistItem(userId, modifier);
},
fetch: [],
});
+13 -1
View File
@@ -1,6 +1,6 @@
import Cards from '/models/cards';
import Checklists from '/models/checklists';
import { allowIsBoardMemberWithWriteAccessByCard } from '/server/lib/utils';
import { allowIsBoardMemberWithWriteAccessByCard, denyCrossBoardMoveByCard } from '/server/lib/utils';
Checklists.allow({
async insert(userId, doc) {
@@ -17,3 +17,15 @@ Checklists.allow({
},
fetch: ['userId', 'cardId'],
});
// Security (GHSA-gv8h-5p3p-6hx7): the allow rule above only checks write access
// on the checklist's SOURCE card/board, so a DDP client could move a checklist
// into a private board it is not a member of by $set-ting a new cardId (the
// Checklists.before.update hook then denormalizes boardId from that destination
// card). Deny any move whose destination board the caller cannot write to.
Checklists.deny({
async update(userId, doc, fieldNames, modifier) {
return await denyCrossBoardMoveByCard(userId, modifier);
},
fetch: [],
});
+4 -4
View File
@@ -1,5 +1,5 @@
name: wekan
version: '9.63'
version: '9.64'
base: core26
build-base: devel
confinement: strict
@@ -194,14 +194,14 @@ parts:
exit 1
;;
esac
WEKAN_ZIP="wekan-9.63-${WEKAN_ARCH}.zip"
WEKAN_ZIP="wekan-9.64-${WEKAN_ARCH}.zip"
NODE_TAR="node-v24.17.0-linux-${NODE_ARCH}.tar.xz"
echo "Cleaning environment first"
rm -rf .build
mkdir .build
cd .build
#wget https://github.com/wekan/wekan/releases/download/v9.63/wekan-9.63-amd64.zip
wget "https://github.com/wekan/wekan/releases/download/v9.63/${WEKAN_ZIP}"
#wget https://github.com/wekan/wekan/releases/download/v9.64/wekan-9.64-amd64.zip
wget "https://github.com/wekan/wekan/releases/download/v9.64/${WEKAN_ZIP}"
unzip "${WEKAN_ZIP}"
rm "${WEKAN_ZIP}"
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3