mirror of
https://github.com/wekan/wekan.git
synced 2026-11-08 21:44:59 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02cf8fdf16 | ||
|
|
b0910e48d4 | ||
|
|
4afd8b8296 | ||
|
|
b1ca76007b | ||
|
|
591a7a458b | ||
|
|
069dbc11f4 |
No files matched your search
@@ -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`
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v9.63.0",
|
||||
"version": "v9.64.0",
|
||||
"description": "Open-Source kanban",
|
||||
"private": true,
|
||||
"repository": {
|
||||
|
||||
@@ -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="<p>The REST API allows you to control and extend Wekan with ease.</p>
|
||||
" 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="<p>The REST API allows you to control and extend Wekan with ease.</p>
|
||||
<p>If you are an end-user and not a dev or a tester, <a href="https://github.com/wekan/wekan/issues/new">create an issue</a> to request new APIs.</p>
|
||||
<blockquote>
|
||||
<p>All API calls in the documentation are made using <code>curl</code>. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.</p>
|
||||
@@ -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,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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user