mirror of
https://github.com/wekan/wekan.git
synced 2026-11-09 14:45:00 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc698806b2 | ||
|
|
0556f01030 | ||
|
|
b379a02631 | ||
|
|
c3f12b5659 | ||
|
|
ea53de4985 | ||
|
|
214b20e7ad |
No files matched your search
@@ -124,6 +124,53 @@ its reason.
|
||||
|
||||
</details>
|
||||
|
||||
# v13.04 2026-11-09 WeKan ® release
|
||||
|
||||
**In short:** **ListLockBleed** is fixed: when a list refuses a role write
|
||||
access, that refusal now also holds for the checklist and checklist-item REST
|
||||
routes. The **Sandstorm** and **Android** bundles retry their npm install when
|
||||
github.com is briefly unavailable.
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUE of [ListLockBleed](https://wekan.fi/hall-of-fame/listlockbleed/):
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/214b20e7ad">A list that refuses a role write access refuses its checklist REST writes too</a>. Thanks to tsurutanmen and xet7.</summary>
|
||||
|
||||
[ListLockBleed](https://wekan.fi/hall-of-fame/listlockbleed/),
|
||||
[GHSA-4734-8vrg-8j3w](https://github.com/wekan/wekan/security/advisories/GHSA-4734-8vrg-8j3w).
|
||||
A board admin can refuse a role write access in one list (#4527). The card
|
||||
and comment REST routes and the DDP checklist writes applied that refusal,
|
||||
but the six checklist and checklist-item REST routes asked only the board. A
|
||||
member the list refused could therefore still create, rename, tick and delete
|
||||
the checklists of its cards with an API token. Those routes now ask the
|
||||
card's list and answer 403, and the refusal shows in Admin Panel / Problems.
|
||||
`tests/listPermissions.test.cjs` fails for any REST write route of the tree
|
||||
that names a card without asking its list. And
|
||||
`tests/playwright/specs/list-lock-checklists.e2e.js` replays the six writes
|
||||
against a running server.
|
||||
|
||||
</details>
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
**Release builds** - what the release workflow does when a download fails.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/c3f12b5659">The Sandstorm and Android bundles retry their npm install through a GitHub outage</a>. Thanks to xet7.</summary>
|
||||
|
||||
v13.03's Sandstorm .spk failed with "npm error 504 Gateway Time-out" for
|
||||
github.com's `meteor/node-source-map-support` tarball: the same kind of
|
||||
outage `releases/npm-retry.sh` has retried since v10.86. That install in
|
||||
`releases/sandstorm-slim-spk.sh`, and the Android bundle's server and sharp
|
||||
installs, ran npm without it, because `tests/npmRetry.test.cjs` read only
|
||||
`release-all.yml`. All three go through the retry now. The test follows
|
||||
every `releases/` script a workflow runs and fails on a bare npm install in
|
||||
any of them.
|
||||
|
||||
</details>
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v13.03 2026-11-09 WeKan ® release
|
||||
|
||||
**In short:** The amd64 **snaps** no longer fail the release when the Ubuntu
|
||||
|
||||
+9
-8
@@ -5,6 +5,15 @@ each one changed and the project status are in
|
||||
Written by `node releases/changelog-short.mjs` from CHANGELOG-big.md:
|
||||
make changes there.
|
||||
|
||||
# v13.04 2026-11-09 WeKan ® release
|
||||
|
||||
**ListLockBleed** is fixed: when a list refuses a role write
|
||||
access, that refusal now also holds for the checklist and checklist-item REST
|
||||
routes. The **Sandstorm** and **Android** bundles retry their npm install when
|
||||
github.com is briefly unavailable.
|
||||
|
||||
[All changes in v13.04](CHANGELOG-big.md#v1304-2026-11-09-wekan--release)
|
||||
|
||||
# v13.03 2026-11-09 WeKan ® release
|
||||
|
||||
The amd64 **snaps** no longer fail the release when the Ubuntu
|
||||
@@ -39,11 +48,3 @@ API documentation** spells the name **WeKan**, so the API pages published on
|
||||
wekan.fi pass its brand-spelling check again.
|
||||
|
||||
[All changes in v13.00](CHANGELOG-big.md#v1300-2026-11-07-wekan--release)
|
||||
|
||||
# v12.99 2026-11-06 WeKan ® release
|
||||
|
||||
The **mac-arm64** release bundle builds again: its job asked for
|
||||
GitHub's retired **macos-14** runner and was cancelled in the v12.98 release
|
||||
run. The **weba** instructions now live in the weba repository itself.
|
||||
|
||||
[All changes in v12.99](CHANGELOG-big.md#v1299-2026-11-06-wekan--release)
|
||||
+1
-1
@@ -38,7 +38,7 @@ FROM --platform=$BUILDPLATFORM debian:trixie AS bundle
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=13.03
|
||||
ARG VERSION=13.04
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
COPY --chmod=755 releases/check-telemetry.py /tmp/check-telemetry.py
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v13.03.0"
|
||||
appVersion: "v13.04.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-13.03-win64.zip](https://github.com/wekan/wekan/releases/download/v13.03/wekan-13.03-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
1. [wekan-13.04-win64.zip](https://github.com/wekan/wekan/releases/download/v13.04/wekan-13.04-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
|
||||
2. [node.exe](https://nodejs.org/dist/latest-v26.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-13.03-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
7. Unzip `wekan-13.04-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
|
||||
```
|
||||
bundle (directory)
|
||||
|
||||
@@ -235,6 +235,10 @@ const CATALOG = {
|
||||
// GHSA-6xqf-jh8x-4wxc: an assigned-only member archiving or converting a card
|
||||
// not assigned to them through archiveSelectedCards / createBoardFromCard.
|
||||
'authz.assigned-card-write': { category: 'authz', bleed: 'UnassignedArchiveBleed', severity: 'medium', cwe: 'CWE-863' },
|
||||
// GHSA-4734-8vrg-8j3w: a REST write in a list whose exception refuses the
|
||||
// caller's role (#4527). The web UI offers no such write, so it is the lock
|
||||
// being stepped round with a token. Medium: recorded, never disables.
|
||||
'authz.list-locked': { category: 'authz', bleed: 'ListLockBleed', severity: 'medium', cwe: 'CWE-863' },
|
||||
'authz.tenant': { category: 'authz', bleed: 'TenantBleed', severity: 'medium', cwe: 'CWE-862' },
|
||||
'authz.search-session': { category: 'authz', bleed: 'SessionBleed', severity: 'medium', cwe: 'CWE-639' },
|
||||
'authz.database': { category: 'authz', bleed: 'DatabaseBleed', severity: 'high', cwe: 'CWE-863' },
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v13.03.0",
|
||||
"version": "v13.04.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wekan",
|
||||
"version": "v13.03.0",
|
||||
"version": "v13.04.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v13.03.0",
|
||||
"version": "v13.04.0",
|
||||
"description": "Open-Source kanban",
|
||||
"private": true,
|
||||
"repository": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
<title>WeKan REST API v13.03</title>
|
||||
<title>WeKan REST API v13.04</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
@@ -484,11 +484,11 @@ data-styled.g658[id="sc-hHttOM"]{content:"dKSNwp,"}/*!sc*/
|
||||
|
||||
<body>
|
||||
|
||||
<div id="redoc"><div hidden=""><script>try{var m=localStorage.getItem("redoc.colorMode");if(m!=="light"&&m!=="dark")m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.classList.add(m)}catch(e){}</script></div><div style="--navbar-height:0px;--navbar-stack-height:0px" class="sc-kIbBWU fFQMWf"><div class="sc-cjUTkN dGsweu menu-content"><div class="sc-jqwVmx jjzLa-d"><div data-testid="search-trigger" class="sc-cToEys dffrbr"><div class="sc-cRcubC ddjiE"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" data-component-name="icons/SearchIcon/SearchIcon" class="sc-cHazmW gTzQi"><path d="M14.5 13.7931L10.724 10.017C11.6313 8.9277 12.0838 7.5305 11.9872 6.11608C11.8907 4.70165 11.2525 3.37891 10.2055 2.423C9.15857 1.4671 7.78336 0.951637 6.36602 0.983845C4.94867 1.01605 3.5983 1.59345 2.59582 2.59593C1.59335 3.5984 1.01595 4.94877 0.983738 6.36612C0.95153 7.78347 1.46699 9.15867 2.4229 10.2057C3.3788 11.2526 4.70155 11.8908 6.11597 11.9873C7.53039 12.0839 8.92759 11.6314 10.0169 10.7241L13.793 14.5001L14.5 13.7931ZM2.00001 6.50012C2.00001 5.6101 2.26393 4.74007 2.7584 4.00005C3.25286 3.26003 3.95567 2.68325 4.77794 2.34266C5.6002 2.00207 6.505 1.91295 7.37792 2.08658C8.25083 2.26022 9.05266 2.6888 9.68199 3.31814C10.3113 3.94747 10.7399 4.7493 10.9135 5.62221C11.0872 6.49513 10.9981 7.39993 10.6575 8.22219C10.3169 9.04446 9.7401 9.74726 9.00008 10.2417C8.26006 10.7362 7.39003 11.0001 6.50001 11.0001C5.30694 10.9988 4.16312 10.5243 3.31949 9.68064C2.47587 8.83701 2.00133 7.69319 2.00001 6.50012Z"></path></svg><span>Loading...</span></div></div></div><div class="sc-dNLPWG dtcefb"><ul data-component-name="Menu/Menu" class="sc-kBPbfe ctPApS"><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-default" role="listitem"><a class="sc-iJONRL iBHKJo" href="/" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="true" class="sc-epufQc fYmDZR"><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="WeKan REST API" class="sc-eStdmu iyAVQU">WeKan REST API</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-default" role="listitem"><a class="sc-iJONRL iBHKJo" href="/section/production-security-concerns" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc itBMtf"><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Production Security Concerns" class="sc-eStdmu liVEkC">Production Security Concerns</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/login" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Login" class="sc-eStdmu liVEkC">Login</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-hgJgCR dlNiXx"></div></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/attachmentstoragesettings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="AttachmentStorageSettings" class="sc-eStdmu liVEkC">AttachmentStorageSettings</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-hgJgCR dlNiXx"></div></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/settings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Settings" classLine truncated
|
||||
<div id="redoc"><div hidden=""><script>try{var m=localStorage.getItem("redoc.colorMode");if(m!=="light"&&m!=="dark")m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.classList.add(m)}catch(e){}</script></div><div style="--navbar-height:0px;--navbar-stack-height:0px" class="sc-kIbBWU fFQMWf"><div class="sc-cjUTkN dGsweu menu-content"><div class="sc-jqwVmx jjzLa-d"><div data-testid="search-trigger" class="sc-cToEys dffrbr"><div class="sc-cRcubC ddjiE"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" data-component-name="icons/SearchIcon/SearchIcon" class="sc-cHazmW gTzQi"><path d="M14.5 13.7931L10.724 10.017C11.6313 8.9277 12.0838 7.5305 11.9872 6.11608C11.8907 4.70165 11.2525 3.37891 10.2055 2.423C9.15857 1.4671 7.78336 0.951637 6.36602 0.983845C4.94867 1.01605 3.5983 1.59345 2.59582 2.59593C1.59335 3.5984 1.01595 4.94877 0.983738 6.36612C0.95153 7.78347 1.46699 9.15867 2.4229 10.2057C3.3788 11.2526 4.70155 11.8908 6.11597 11.9873C7.53039 12.0839 8.92759 11.6314 10.0169 10.7241L13.793 14.5001L14.5 13.7931ZM2.00001 6.50012C2.00001 5.6101 2.26393 4.74007 2.7584 4.00005C3.25286 3.26003 3.95567 2.68325 4.77794 2.34266C5.6002 2.00207 6.505 1.91295 7.37792 2.08658C8.25083 2.26022 9.05266 2.6888 9.68199 3.31814C10.3113 3.94747 10.7399 4.7493 10.9135 5.62221C11.0872 6.49513 10.9981 7.39993 10.6575 8.22219C10.3169 9.04446 9.7401 9.74726 9.00008 10.2417C8.26006 10.7362 7.39003 11.0001 6.50001 11.0001C5.30694 10.9988 4.16312 10.5243 3.31949 9.68064C2.47587 8.83701 2.00133 7.69319 2.00001 6.50012Z"></path></svg><span>Loading...</span></div></div></div><div class="sc-dNLPWG dtcefb"><ul data-component-name="Menu/Menu" class="sc-kBPbfe ctPApS"><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-default" role="listitem"><a class="sc-iJONRL iBHKJo" href="/" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="true" class="sc-epufQc fYmDZR"><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="WeKan REST API" class="sc-eStdmu iyAVQU">WeKan REST API</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-default" role="listitem"><a class="sc-iJONRL iBHKJo" href="/section/production-security-concerns" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc itBMtf"><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Production Security Concerns" class="sc-eStdmu liVEkC">Production Security Concerns</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/login" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Login" class="sc-eStdmu liVEkC">Login</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-hgJgCR dlNiXx"></div></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/attachmentstoragesettings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="AttachmentStorageSettings" class="sc-eStdmu liVEkC">AttachmentStorageSettings</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-hgJgCR dlNiXx"></div></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/settings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Settings" classLine truncated
|
||||
<script type="module">
|
||||
import { hydrate } from "https://cdn.redocly.com/redoc/v3.0.1/bundles/redoc.standalone.js";
|
||||
|
||||
const __redoc_definition = {"openapi":"3.2.0","info":{"title":"WeKan REST API","version":"v13.03","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 `\u003cfield>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 global admin can call this. CountsLine truncated
|
||||
const __redoc_definition = {"openapi":"3.2.0","info":{"title":"WeKan REST API","version":"v13.04","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 `\u003cfield>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 global admin can call this. CountsLine truncated
|
||||
const __redoc_options = {"skipBundle":true,"disableTelemetry":false};
|
||||
|
||||
hydrate(__redoc_definition, __redoc_options, document.getElementById('redoc'));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: WeKan REST API
|
||||
version: v13.03
|
||||
version: v13.04
|
||||
description: |
|
||||
The REST API allows you to control and extend WeKan with ease.
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ fi
|
||||
# have no Android prebuild, were never compiled, and WeKan did not start. It
|
||||
# is idempotent, so a bundle already prepared is left as it is.
|
||||
"$node" "$root/releases/prepare-bundle-npm.mjs" "$root/bundle"
|
||||
(cd "$server" && npm install)
|
||||
(cd "$server" && bash "$root/releases/npm-retry.sh" npm install)
|
||||
# accounts-password requires both at startup with no fallback. Load them here
|
||||
# with Android's Node.js, so a missing addon stops the build with its name -
|
||||
# check-addon-arch below passes Linux prebuilds, which Android does not load.
|
||||
@@ -86,7 +86,7 @@ for (const name of ["argon2", "bcrypt"]) {
|
||||
catch (error) { console.error(`::error::${name} has no Android addon: ${error.message.split("\n")[0]}`); process.exit(1); }
|
||||
}')
|
||||
# sharp has no Android prebuilt: built from source against Termux's libvips.
|
||||
if ! (cd "$appmods/sharp" && npm install --no-save --omit=dev node-addon-api node-gyp && "$node" install/build.js); then
|
||||
if ! (cd "$appmods/sharp" && bash "$root/releases/npm-retry.sh" npm install --no-save --omit=dev node-addon-api node-gyp && "$node" install/build.js); then
|
||||
echo "::warning::sharp could not be built against Termux's libvips here; it runs as WebAssembly (@img/sharp-wasm32)."
|
||||
fi
|
||||
"$node" "$root/releases/prune-build-only-modules.mjs" "$root/bundle"
|
||||
|
||||
@@ -67,7 +67,9 @@ echo "==> [2/5] meteor build (Sandstorm profile)"
|
||||
rm -rf .meteor-spk/bundle
|
||||
mkdir -p .meteor-spk
|
||||
"$METEOR" build --directory .meteor-spk
|
||||
(cd .meteor-spk/bundle/programs/server && "$METEOR" npm install)
|
||||
# Through npm-retry.sh, as every other bundle's install: v13.03's .spk died
|
||||
# here on a 504 from github.com for meteor/node-source-map-support's tarball.
|
||||
(cd .meteor-spk/bundle/programs/server && bash "$REPO/releases/npm-retry.sh" "$METEOR" npm install)
|
||||
|
||||
echo "==> [3/5] trim the bundle to a linux/x64 sockjs grain"
|
||||
# A grain is linux/x64, always, and talks DDP over sockjs (sandstorm-pkgdef.capnp
|
||||
|
||||
@@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
|
||||
appTitle = (defaultText = "Wekan"),
|
||||
# The name of the app as it is displayed to the user.
|
||||
|
||||
appVersion = 1303,
|
||||
appVersion = 1304,
|
||||
# Increment this for every release.
|
||||
|
||||
appMarketingVersion = (defaultText = "13.03~2026-11-09"),
|
||||
appMarketingVersion = (defaultText = "13.04~2026-11-09"),
|
||||
# Human-readable presentation of the app version.
|
||||
|
||||
minUpgradableAppVersion = 0,
|
||||
|
||||
@@ -33,9 +33,19 @@ export async function canCommentInList(userId, board, listId) {
|
||||
* The REST API's half: a list that refuses this user's role refuses the API
|
||||
* too (403), so a restriction cannot be stepped round with a token. A list's
|
||||
* GRANT does not open the API to a role the board does not let write.
|
||||
*
|
||||
* ListLockBleed (GHSA-4734-8vrg-8j3w): the checklist and checklist-item routes
|
||||
* did not ask, so a member a list refused still wrote its cards' checklists
|
||||
* over REST. Every REST write that names a card asks now
|
||||
* (tests/listPermissions.test.cjs), and the refusal is recorded in Admin
|
||||
* Panel -> Problems: no screen offers such a write, so it is an attempt.
|
||||
*/
|
||||
export async function assertListNotRefused(userId, board, listId, capability = 'write') {
|
||||
export async function assertListNotRefused(userId, board, listId, capability = 'write', source = 'rest:list-locked') {
|
||||
if ((await listException(userId, board, listId, capability)) !== false) return;
|
||||
try {
|
||||
require('/server/lib/securityLog').record({ key: 'authz.list-locked', action: 'blocked', source, userId,
|
||||
detail: `A REST ${capability} in a list whose exception refuses the caller's role.` });
|
||||
} catch (e) { /* logging must never break the guard */ }
|
||||
const { Meteor } = require('meteor/meteor');
|
||||
const error = new Meteor.Error('Forbidden', 'Forbidden');
|
||||
error.statusCode = 403;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor';
|
||||
import { WebApp } from 'meteor/webapp';
|
||||
import { Authentication } from '/server/authentication';
|
||||
import { sendJsonResult } from '/server/apiMiddleware';
|
||||
import { assertListNotRefused } from '/server/lib/listPermissionGate';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import ChecklistItems, {
|
||||
itemCreation,
|
||||
@@ -145,6 +146,8 @@ WebApp.handlers.post(
|
||||
if (checklist) {
|
||||
const card = await ReactiveCache.getCard(paramCardId);
|
||||
if (card && card.boardId === paramBoardId) {
|
||||
// ListLockBleed (GHSA-4734-8vrg-8j3w): the list's refusal holds over REST too (#4527).
|
||||
await assertListNotRefused(req.userId, await ReactiveCache.getBoard(paramBoardId), card.listId, 'write', 'rest:checklist-item');
|
||||
// Append, like the UI does. Every item created over REST used to be given
|
||||
// `sort: 0`, so a checklist filled through the API came out in an order
|
||||
// nobody chose - and every item after the first shared the first one's
|
||||
@@ -214,6 +217,8 @@ WebApp.handlers.put(
|
||||
});
|
||||
return;
|
||||
}
|
||||
// ListLockBleed (GHSA-4734-8vrg-8j3w): the list's refusal holds over REST too (#4527).
|
||||
await assertListNotRefused(req.userId, await ReactiveCache.getBoard(paramBoardId), card.listId, 'write', 'rest:checklist-item');
|
||||
|
||||
const deadline = parseDueAt(req.body);
|
||||
if (deadline.error) {
|
||||
@@ -277,6 +282,8 @@ WebApp.handlers.delete(
|
||||
});
|
||||
return;
|
||||
}
|
||||
// ListLockBleed (GHSA-4734-8vrg-8j3w): the list's refusal holds over REST too (#4527).
|
||||
await assertListNotRefused(req.userId, await ReactiveCache.getBoard(paramBoardId), card.listId, 'write', 'rest:checklist-item');
|
||||
|
||||
await ChecklistItems.direct.removeAsync({ _id: paramItemId });
|
||||
sendJsonResult(res, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { parseDueAt, checklistWithItems } from '/server/lib/checklistDeadlines';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { WebApp } from 'meteor/webapp';
|
||||
import { assertListNotRefused } from '/server/lib/listPermissionGate';
|
||||
import { Authentication } from '/server/authentication';
|
||||
import { sendJsonResult } from '/server/apiMiddleware';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
@@ -266,6 +267,8 @@ WebApp.handlers.post(
|
||||
});
|
||||
return;
|
||||
}
|
||||
// ListLockBleed (GHSA-4734-8vrg-8j3w): the list's refusal holds over REST too (#4527).
|
||||
await assertListNotRefused(req.userId, await ReactiveCache.getBoard(paramBoardId), card.listId, 'write', 'rest:checklist');
|
||||
|
||||
const deadline = parseDueAt(req.body);
|
||||
if (deadline.error) {
|
||||
@@ -335,6 +338,8 @@ WebApp.handlers.put(
|
||||
});
|
||||
return;
|
||||
}
|
||||
// ListLockBleed (GHSA-4734-8vrg-8j3w): the list's refusal holds over REST too (#4527).
|
||||
await assertListNotRefused(req.userId, await ReactiveCache.getBoard(paramBoardId), card.listId, 'write', 'rest:checklist');
|
||||
|
||||
const checklist = await ReactiveCache.getChecklist({
|
||||
_id: paramChecklistId,
|
||||
@@ -390,6 +395,8 @@ WebApp.handlers.delete(
|
||||
});
|
||||
return;
|
||||
}
|
||||
// ListLockBleed (GHSA-4734-8vrg-8j3w): the list's refusal holds over REST too (#4527).
|
||||
await assertListNotRefused(req.userId, await ReactiveCache.getBoard(paramBoardId), card.listId, 'write', 'rest:checklist');
|
||||
|
||||
const checklist = await ReactiveCache.getChecklist({
|
||||
_id: paramChecklistId,
|
||||
|
||||
+4
-4
@@ -19,7 +19,7 @@
|
||||
# base: core24 (not core26) · no build-base: devel · grade: stable
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: wekan
|
||||
version: '13.03'
|
||||
version: '13.04'
|
||||
base: core24
|
||||
confinement: strict
|
||||
grade: stable
|
||||
@@ -703,18 +703,18 @@ parts:
|
||||
armhf) WEKAN_ARCH="armhf" ;;
|
||||
*) echo "Unsupported architecture: ${CRAFT_ARCH_BUILD_FOR}"; exit 1 ;;
|
||||
esac
|
||||
WEKAN_ZIP="wekan-13.03-${WEKAN_ARCH}.zip"
|
||||
WEKAN_ZIP="wekan-13.04-${WEKAN_ARCH}.zip"
|
||||
echo "Cleaning environment first"
|
||||
rm -rf .build
|
||||
mkdir .build
|
||||
cd .build
|
||||
#wget https://github.com/wekan/wekan/releases/download/v13.03/wekan-13.03-amd64.zip
|
||||
#wget https://github.com/wekan/wekan/releases/download/v13.04/wekan-13.04-amd64.zip
|
||||
# Retry: a release asset can 404 for a short while right after it is
|
||||
# uploaded (CDN/propagation lag), and a plain wget treated that as
|
||||
# fatal. --retry-on-http-error covers the 404/5xx window; a genuinely
|
||||
# missing bundle still fails after the tries are spent.
|
||||
wget --tries=20 --waitretry=20 --retry-on-http-error=404,403,500,502,503 \
|
||||
"https://github.com/wekan/wekan/releases/download/v13.03/${WEKAN_ZIP}"
|
||||
"https://github.com/wekan/wekan/releases/download/v13.04/${WEKAN_ZIP}"
|
||||
unzip "${WEKAN_ZIP}"
|
||||
python3 ../releases/check-telemetry.py --bundle bundle
|
||||
rm "${WEKAN_ZIP}"
|
||||
|
||||
@@ -37,7 +37,7 @@ test('the bundle is rebuilt by the Android Node.js itself, checked and started',
|
||||
// happened in programs/server/node_modules, where sharp is not.
|
||||
assert.match(build, /appmods="\$server\/npm\/node_modules"\nrm -rf "\$appmods\/@img\/sharp-linux-"\*/);
|
||||
assert.doesNotMatch(build, /rm -rf "\$server\/node_modules\/@img/, 'not the wrong directory');
|
||||
assert.match(build, /cd "\$appmods\/sharp" && npm install --no-save --omit=dev node-addon-api node-gyp && "\$node" install\/build\.js/);
|
||||
assert.match(build, /cd "\$appmods\/sharp" && bash "\$root\/releases\/npm-retry\.sh" npm install --no-save --omit=dev node-addon-api node-gyp && "\$node" install\/build\.js/);
|
||||
assert.match(build, /::warning::sharp could not be built[^\n]*WebAssembly/);
|
||||
// Termux's user does not own the bind mount: chmod only what is not
|
||||
// already executable (v12.43-v12.44 stopped at "Operation not permitted").
|
||||
@@ -57,7 +57,9 @@ test('argon2 and bcrypt are compiled for Android and must load before the bundle
|
||||
// was found for platform=android" until the 240 s smoke test gave up.
|
||||
const build = read('releases/android/build-bundle.sh');
|
||||
const prepare = build.indexOf('"$node" "$root/releases/prepare-bundle-npm.mjs" "$root/bundle"');
|
||||
const install = build.indexOf('(cd "$server" && npm install)');
|
||||
// Through npm-retry.sh since v13.03's Sandstorm .spk was lost to a 504 from
|
||||
// github.com (tests/npmRetry.test.cjs): the same install, retried on outage.
|
||||
const install = build.indexOf('(cd "$server" && bash "$root/releases/npm-retry.sh" npm install)');
|
||||
const loads = build.indexOf('for (const name of ["argon2", "bcrypt"])');
|
||||
assert.ok(prepare > 0 && prepare < install, 'the bundle declares its installers before npm installs');
|
||||
assert.ok(loads > install && loads < build.indexOf('check-addon-arch.mjs'), 'loaded with Android Node.js right after the install');
|
||||
@@ -103,7 +105,7 @@ test('termux-exec is preloaded before npm, so #!/usr/bin/env scripts run', () =>
|
||||
assert(preload > 0, 'LD_PRELOAD is exported');
|
||||
assert.match(build, /"\$PREFIX\/lib\/libtermux-exec-ld-preload\.so" "\$PREFIX\/lib\/libtermux-exec\.so"/, 'both termux-exec generations, newest first, as login does');
|
||||
assert.match(build, /LD_PRELOAD="\$lib" sh -c true/, 'kept only if a program still starts with it');
|
||||
assert(preload < build.indexOf('(cd "$server" && npm install)'), 'before npm runs any install script');
|
||||
assert(preload < build.indexOf('(cd "$server" && bash "$root/releases/npm-retry.sh" npm install)'), 'before npm runs any install script');
|
||||
});
|
||||
|
||||
test('negative: an LD_PRELOAD already set is not replaced', () => {
|
||||
|
||||
@@ -30,7 +30,9 @@ assert.equal(parseDueAt({ dueAt: '2026-09-15T14:30:00+02:00' }).dueAt.toISOStrin
|
||||
vm.runInNewContext(src.slice(start, end), {
|
||||
WebApp: { handlers: { put: (route, fn) => { handler = fn; } } }, parseDueAt,
|
||||
Authentication: { checkBoardWriteAccess: async () => { if (denied) throw Error('denied'); } },
|
||||
ReactiveCache: { getCard: async () => wrongCard ? null : card, getChecklist: async () => ({ _id: 'check' }) },
|
||||
ReactiveCache: { getCard: async () => wrongCard ? null : card, getChecklist: async () => ({ _id: 'check' }), getBoard: async () => ({ _id: 'board' }) },
|
||||
// ListLockBleed's list check: tests/listPermissions.test.cjs pins it; here the list allows.
|
||||
assertListNotRefused: async () => {},
|
||||
Checklists: { direct: { updateAsync: async (...args) => updates.push(args) } },
|
||||
sendJsonResult: (res, data) => { response = data; },
|
||||
});
|
||||
@@ -51,7 +53,8 @@ assert.equal(parseDueAt({ dueAt: '2026-09-15T14:30:00+02:00' }).dueAt.toISOStrin
|
||||
vm.runInNewContext(itemSource.slice(itemStart, itemEnd), {
|
||||
WebApp: { handlers: { put: (route, fn) => { itemHandler = fn; } } }, parseDueAt,
|
||||
Authentication: { checkBoardWriteAccess: async () => {} },
|
||||
ReactiveCache: { getChecklistItem: async () => ({ _id: 'item', cardId: 'card', checklistId: 'check' }), getCard: async () => ({ _id: 'card', boardId: 'board' }) },
|
||||
ReactiveCache: { getChecklistItem: async () => ({ _id: 'item', cardId: 'card', checklistId: 'check' }), getCard: async () => ({ _id: 'card', boardId: 'board' }), getBoard: async () => ({ _id: 'board' }) },
|
||||
assertListNotRefused: async () => {},
|
||||
ChecklistItems: { direct: { updateAsync: async (...args) => itemUpdates.push(args) } },
|
||||
sendJsonResult: (res, data) => { itemResponse = data; },
|
||||
});
|
||||
|
||||
@@ -57,7 +57,11 @@ function fixture() {
|
||||
getChecklistItems: async () => [],
|
||||
getCard: async id => cards[id] || null,
|
||||
getChecklist: async q => (checklists[q._id] && checklists[q._id].cardId === q.cardId ? checklists[q._id] : null),
|
||||
getBoard: async id => ({ _id: id }),
|
||||
},
|
||||
// ListLockBleed's list check (tests/listPermissions.test.cjs pins it); the
|
||||
// lists of this fixture refuse nobody.
|
||||
assertListNotRefused: async () => {},
|
||||
ChecklistItems: {
|
||||
insertAsync: async doc => { inserted.push(doc); return 'new'; },
|
||||
direct: {
|
||||
|
||||
@@ -130,6 +130,73 @@ test('the REST API honours a list\'s refusal on every per-list card route', () =
|
||||
assert.match(read('server/models/cardComments.js'), /canCommentInList\(req\.userId, board, card\.listId\)/, 'comments too');
|
||||
});
|
||||
|
||||
// ListLockBleed (GHSA-4734-8vrg-8j3w): the checklist and checklist-item REST
|
||||
// routes asked only the board, so a member a list refused still wrote its
|
||||
// cards' checklists with a token. Not one route at a time: EVERY REST write in
|
||||
// the tree that names a card must ask the list - or be named here, with why.
|
||||
test('negative: every REST write that names a card asks the list (ListLockBleed)', () => {
|
||||
const unrelated = {
|
||||
// A comment's edit and deletion follow the comment's own rule (its author,
|
||||
// or a board admin: assertCanMutateComment), on DDP as over REST; a list's
|
||||
// comment refusal stops NEW comments, which the POST route asks.
|
||||
'PUT /api/boards/:boardId/cards/:cardId/comments/:commentId': 'comment rule',
|
||||
'DELETE /api/boards/:boardId/cards/:cardId/comments/:commentId': 'comment rule',
|
||||
// Dependency lines have their own board-wide rule (canEditBoardDependencies),
|
||||
// the same on DDP (setBoardDependency) as over REST.
|
||||
'POST /api/boards/:boardId/cards/:cardId/dependencies': 'dependency rule',
|
||||
'PUT /api/boards/:boardId/cards/:cardId/dependencies/:targetId': 'dependency rule',
|
||||
'DELETE /api/boards/:boardId/cards/:cardId/dependencies/:targetId': 'dependency rule',
|
||||
// A bot runner's report of its own run, written to BotRuns, not the card.
|
||||
'POST /api/boards/:boardId/cards/:cardId/bot-runs/:runId': 'bot run report',
|
||||
};
|
||||
const found = [], missing = [];
|
||||
const walk = dir => {
|
||||
for (const entry of fs.readdirSync(path.join(ROOT, dir), { withFileTypes: true })) {
|
||||
const rel = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) { if (!['tests', 'node_modules'].includes(entry.name)) walk(rel); continue; }
|
||||
if (!/\.js$/.test(entry.name)) continue;
|
||||
const src = read(rel);
|
||||
for (const m of src.matchAll(/WebApp\.handlers\.(post|put|delete|patch)\(\s*['"`]([^'"`]*:cardId[^'"`]*)['"`]/g)) {
|
||||
const route = `${m[1].toUpperCase()} ${m[2]}`;
|
||||
found.push(route);
|
||||
const next = src.indexOf('WebApp.handlers.', m.index + 1);
|
||||
let body = src.slice(m.index, next < 0 ? undefined : next);
|
||||
const delegate = /await (\w+)\(req, res\b/.exec(body);
|
||||
if (delegate) {
|
||||
const start = src.indexOf(`async function ${delegate[1]}(`);
|
||||
if (start >= 0) body += src.slice(start, src.indexOf('\n}\n', start));
|
||||
}
|
||||
if (!/assertListNotRefused\(|canWriteInList\(|canCommentInList\(/.test(body) && !unrelated[route]) missing.push(`${route} (${rel})`);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk('server');
|
||||
walk('models');
|
||||
assert.ok(found.length >= 20, `the REST card write routes are found (${found.length})`);
|
||||
for (const route of ['POST /api/boards/:boardId/cards/:cardId/checklists', 'PUT /api/boards/:boardId/cards/:cardId/checklists/:checklistId',
|
||||
'DELETE /api/boards/:boardId/cards/:cardId/checklists/:checklistId', 'POST /api/boards/:boardId/cards/:cardId/checklists/:checklistId/items',
|
||||
'PUT /api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
|
||||
'DELETE /api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId']) {
|
||||
assert.ok(found.includes(route), `${route} is scanned`);
|
||||
}
|
||||
assert.deepEqual(missing, []);
|
||||
// The exceptions are real routes: a renamed one must be looked at again.
|
||||
for (const route of Object.keys(unrelated)) assert.ok(found.includes(route), `${route} still exists`);
|
||||
});
|
||||
|
||||
test('the refusal is recorded in Admin Panel -> Problems, and never breaks the guard (ListLockBleed)', () => {
|
||||
const gate = read('server/lib/listPermissionGate.js');
|
||||
const fn = gate.slice(gate.indexOf('export async function assertListNotRefused('));
|
||||
assert.match(fn, /record\(\{ key: 'authz\.list-locked', action: 'blocked'/);
|
||||
assert.match(fn, /catch \(e\) \{ \/\* logging must never break the guard \*\/ \}/);
|
||||
// Recorded only on the refusal: after the early return of an allowed write.
|
||||
assert.ok(fn.indexOf("!== false) return;") < fn.indexOf('record('), 'an allowed write records nothing');
|
||||
const { categoryFor } = require('../models/lib/securityCategories');
|
||||
const cat = categoryFor('authz.list-locked');
|
||||
assert.equal(cat.bleed, 'ListLockBleed');
|
||||
assert.equal(cat.severity, 'medium', 'recorded, never disables the account');
|
||||
});
|
||||
|
||||
test('the client offers what the list allows', () => {
|
||||
const utils = read('client/lib/utils.js');
|
||||
assert.match(utils, /canModifyCard\([\s\S]*?return Utils\.currentUserCanInList\('write', card, board\);/);
|
||||
|
||||
@@ -141,6 +141,46 @@ test('every npm install in the release goes through it', () => {
|
||||
'these npm commands can lose a release to a five-minute outage');
|
||||
});
|
||||
|
||||
test('every npm install in a script a workflow runs goes through it too', () => {
|
||||
// The test above read only release-all.yml, so a bare install one level down
|
||||
// passed it: v13.03's Sandstorm .spk died on a 504 from github.com inside
|
||||
// releases/sandstorm-slim-spk.sh, and the Android bundle had the same line.
|
||||
// So read every releases/ script any workflow names, and the scripts those
|
||||
// name in turn.
|
||||
const wfDir = path.join(repoRoot, '.github/workflows');
|
||||
const seen = new Set();
|
||||
const queue = [];
|
||||
// Only command lines name a script that runs: a commented-out `scp` of one
|
||||
// does not run it.
|
||||
const scan = text => {
|
||||
const code = text.split('\n').filter(l => !/^\s*#/.test(l)).join('\n');
|
||||
for (const m of code.matchAll(/releases\/[\w./-]+\.sh\b/g)) {
|
||||
const rel = m[0];
|
||||
if (!seen.has(rel) && fs.existsSync(path.join(repoRoot, rel))) { seen.add(rel); queue.push(rel); }
|
||||
}
|
||||
};
|
||||
fs.readdirSync(wfDir).filter(f => /\.ya?ml$/.test(f))
|
||||
.forEach(f => scan(fs.readFileSync(path.join(wfDir, f), 'utf8')));
|
||||
const bare = [];
|
||||
while (queue.length) {
|
||||
const rel = queue.shift();
|
||||
const text = fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
||||
scan(text);
|
||||
text.split('\n').forEach((line, i) => {
|
||||
if (/^\s*#/.test(line)) return;
|
||||
if (!/\bnpm (install|ci)\b/.test(line)) return;
|
||||
if (line.includes('npm-retry.sh')) return;
|
||||
if (line.includes('|| true')) return; // best-effort, as above
|
||||
if (/\bgrep\b/.test(line)) return; // looking for the line, not running it
|
||||
if (/^\s*(echo|printf)\b|"[^"]*\bnpm (install|ci)\b[^"]*"\s*(>&2|;|\)|$)/.test(line) && !/\(cd /.test(line)) return; // a message, not a command
|
||||
bare.push(`${rel}:${i + 1}: ${line.trim()}`);
|
||||
});
|
||||
}
|
||||
assert.ok(seen.has('releases/sandstorm-slim-spk.sh'), 'the Sandstorm pack script is among the scripts read');
|
||||
assert.deepStrictEqual(bare, [],
|
||||
'these npm commands, run by a workflow, can lose a release to a five-minute outage');
|
||||
});
|
||||
|
||||
test('the containers can reach the script that retries for them', () => {
|
||||
// Two of these installs run inside `docker run`, where the repository is not
|
||||
// present unless it is mounted. A call to /releases/npm-retry.sh in a
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
// ListLockBleed (GHSA-4734-8vrg-8j3w): a list whose exception refuses a role
|
||||
// write (#4527) refuses that role's checklist and checklist-item writes over
|
||||
// REST too, and the attempt is in Admin Panel -> Problems. A list without the
|
||||
// refusal still takes the same writes.
|
||||
const { test, expect } = require('../fixtures');
|
||||
const db = require('../helpers/db');
|
||||
|
||||
test('a list refusing a role write refuses its checklist REST writes (ListLockBleed)', async ({ request, board, user, user2 }) => {
|
||||
const original = db.getBoard(board.boardId).members;
|
||||
const [lockedListId, openListId] = board.listIds;
|
||||
const card = id => db.find('cards', { boardId: board.boardId, listId: id })[0];
|
||||
const locked = card(lockedListId), open = card(openListId);
|
||||
const api = (method, url, data) => request[method](url, {
|
||||
headers: { Authorization: `Bearer ${user2.token}`, 'Content-Type': 'application/json' }, ...(data ? { data } : {}) });
|
||||
const recorded = () => db.findOne('eventlog', { bleed: 'ListLockBleed' })?.count || 0;
|
||||
db.updateOne('boards', { _id: board.boardId }, { $set: { members: [...original.filter(m => m.userId !== user2.id),
|
||||
{ userId: user2.id, isActive: true, isAdmin: false }] } });
|
||||
db.updateOne('lists', { _id: lockedListId }, { $set: { roleExceptions: { normal: { write: false } } } });
|
||||
const checklist = { _id: db.uid('locked-checklist'), cardId: locked._id, title: 'Locked', sort: 0 };
|
||||
const item = { _id: db.uid('locked-item'), cardId: locked._id, checklistId: checklist._id, title: 'Locked item', sort: 0, isFinished: false };
|
||||
db.insertOne('checklists', checklist);
|
||||
db.insertOne('checklistItems', item);
|
||||
try {
|
||||
const before = recorded();
|
||||
const base = `/api/boards/${board.boardId}/cards/${locked._id}/checklists`;
|
||||
for (const [method, url, data] of [
|
||||
['post', base, { title: 'x' }],
|
||||
['put', `${base}/${checklist._id}`, { title: 'renamed' }],
|
||||
['post', `${base}/${checklist._id}/items`, { title: 'new item' }],
|
||||
['put', `${base}/${checklist._id}/items/${item._id}`, { isFinished: 'true', title: 'changed' }],
|
||||
['delete', `${base}/${checklist._id}/items/${item._id}`],
|
||||
['delete', `${base}/${checklist._id}`],
|
||||
]) {
|
||||
const res = await api(method, url, data);
|
||||
expect(res.status(), `${method.toUpperCase()} ${url}`).toBe(403);
|
||||
}
|
||||
expect(db.find('checklists', { cardId: locked._id })).toEqual([expect.objectContaining({ _id: checklist._id, title: 'Locked' })]);
|
||||
expect(db.find('checklistItems', { cardId: locked._id })).toEqual([expect.objectContaining({ _id: item._id, title: 'Locked item', isFinished: false })]);
|
||||
expect(recorded()).toBeGreaterThan(before);
|
||||
expect(db.findOne('users', { _id: user2.id }).loginDisabled || false).toBe(false);
|
||||
|
||||
// Negative: the same member writes the checklists of a list without the refusal.
|
||||
const made = await api('post', `/api/boards/${board.boardId}/cards/${open._id}/checklists`, { title: 'Open', items: ['one'] });
|
||||
expect(made.status()).toBe(200);
|
||||
const openId = (await made.json())._id;
|
||||
expect((await api('put', `/api/boards/${board.boardId}/cards/${open._id}/checklists/${openId}`, { title: 'Open renamed' })).status()).toBe(200);
|
||||
expect((await api('delete', `/api/boards/${board.boardId}/cards/${open._id}/checklists/${openId}`)).status()).toBe(200);
|
||||
// And the board admin, whom no list exception binds, writes the locked list's.
|
||||
const admin = await request.post(base, { headers: { Authorization: `Bearer ${user.token}`, 'Content-Type': 'application/json' }, data: { title: 'Admin' } });
|
||||
expect(admin.status()).toBe(200);
|
||||
} finally {
|
||||
db.updateOne('boards', { _id: board.boardId }, { $set: { members: original } });
|
||||
db.updateOne('lists', { _id: lockedListId }, { $unset: { roleExceptions: '' } });
|
||||
db.deleteMany('checklists', { cardId: { $in: [locked._id, open._id] } });
|
||||
db.deleteMany('checklistItems', { cardId: { $in: [locked._id, open._id] } });
|
||||
}
|
||||
});
|
||||
@@ -221,6 +221,7 @@ const GUARDED = {
|
||||
importplacementbleed: ['tests/importPlacementBleed.test.cjs', 'tests/playwright/specs/security-import-placement.e2e.js'],
|
||||
samlsubjectbleed: ['tests/samlIdentityBinding.test.cjs'],
|
||||
stampbleed: ['tests/stampBleed.test.cjs', 'tests/playwright/specs/upload-stamp-bleed.e2e.js'],
|
||||
listlockbleed: ['tests/listPermissions.test.cjs', 'tests/playwright/specs/list-lock-checklists.e2e.js'],
|
||||
unassignedarchivebleed: ['tests/assignedCardMethods.test.cjs', 'tests/playwright/specs/assigned-card-methods.e2e.js'],
|
||||
unassignedattachbleed: ['tests/unassignedAttachBleed.test.cjs', 'tests/playwright/specs/unassigned-attach-bleed.e2e.js'],
|
||||
uploadbleed: ['tests/uploadBleed.test.cjs', 'tests/playwright/specs/upload-stamp-bleed.e2e.js'],
|
||||
@@ -373,7 +374,8 @@ test('the whole published list is accounted for', () => {
|
||||
// unnoticed until the companion checkout moved to .tools/w/wekan.fi and this
|
||||
// file looked there: at the old path the comparison was skipped.
|
||||
// 145 -> 146: UnassignedArchiveBleed (GHSA-6xqf-jh8x-4wxc, 2026-11-08), guarded.
|
||||
assert.strictEqual(total, 146, 'the Hall of Fame and this list disagree on how many there are');
|
||||
// 146 -> 147: ListLockBleed (GHSA-4734-8vrg-8j3w, 2026-11-09), guarded.
|
||||
assert.strictEqual(total, 147, 'the Hall of Fame and this list disagree on how many there are');
|
||||
});
|
||||
|
||||
test('the companion Hall of Fame names match the inventory when available', () => {
|
||||
|
||||
Reference in new issue
Block a user