fix: webdav LOCK was not checking permissions correctly
Massimo Melina committed
Jul 24, 2026 at 12:36 UTC
0c5548a2b6f60f94f4ed951e6707758fe9c889b3
2 files changed
+48
src/webdav.ts
+12
@@ -259,6 +259,18 @@ export const webdav: Koa.Middleware = async (ctx, next) => {
259
260
async function handleLock() {
261
setWebdavHeaders()
262
+ // a lock reserves a future write, so authorize it against the existing resource or its parent
263
+ const node = await urlToNode(path, ctx)
264
+ const permissionNode = node || await urlToNode(dirname(path), ctx)
265
+ if (!permissionNode)
266
+ return ctx.status = HTTP_CONFLICT
267
+ const missingWritePerm = node && canOverwrite.has(path + prefix('|', getCurrentUsername(ctx))) ? 0
268
+ : statusCodeForMissingPerm(permissionNode, node ? 'can_delete' : 'can_upload', ctx)
269
+ if (missingWritePerm) {
270
+ if (ctx.status === HTTP_UNAUTHORIZED)
271
+ setWebdavHeaders(true)
272
+ return
273
+ }
274
const body = ctx.length || ctx.get('content-length') || ctx.get('transfer-encoding') ? await stream2string(ctx.req) : ''
275
const token = getProvidedLockToken()
276
let seconds = Number(ctx.get('timeout').split(',').find(x => /^Second-\d+$/i.test(x.trim()))?.trim().split('-', 2)[1])
tests/test.ts
+36
@@ -563,6 +563,42 @@ describe('webdav', () => {
563
await rmAny(destPath)
564
}
565
})
566
+ test('webdav.lock requires write permission', async () => {
567
+ const user = `wd-lock-readonly-${randomId(6)}`.toLowerCase()
568
+ const password = randomId(10)
569
+ const uri = '/tests/page/gpl.png'
570
+ let token = ''
571
+ const adminReq = { auth, jar: {} }
572
+ try {
573
+ await reqApi('add_account', { username: user, password }, res => res?.username === user, adminReq)()
574
+ await req(uri, 401, {
575
+ method: 'LOCK',
576
+ auth: `${user}:${password}`,
577
+ jar: {},
578
+ headers: { 'content-type': 'text/xml', 'user-agent': WEBDAV_UA },
579
+ body: WEBDAV_LOCK_BODY,
580
+ })()
581
+ await webdavLock(uri, (_data, res) => token = res.headers?.[TOKEN_HEADER] || '')()
582
+ }
583
+ finally {
584
+ if (token)
585
+ await webdavUnlock(uri, token)().catch(() => {})
586
+ await reqApi('del_account', { username: user }, 200, adminReq)().catch(() => {})
587
+ }
588
+ })
589
+ test('webdav.lock allows a missing upload destination', async () => {
590
+ const uri = `${UPLOAD_ROOT}wd-lock-missing-${randomId(6)}.txt`
591
+ let token = ''
592
+ try {
593
+ await webdavLock(uri, (_data, res) => token = res.headers?.[TOKEN_HEADER] || '')()
594
+ if (!token)
595
+ throw "missing lock token"
596
+ }
597
+ finally {
598
+ if (token)
599
+ await webdavUnlock(uri, token)().catch(() => {})
600
+ }
601
+ })
602
test('webdav.lock refresh keeps token', async () => {
603
const name = `wd-lock-${randomId(6)}.txt`
604
const uri = `${UPLOAD_ROOT}${UPLOAD_DIR}/${name}`