@samitouri / QOSami-HFS / commits / 0c5548a2

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}`