fix: webdav not working when filenames have "&"

Massimo Melina committed Mar 15, 2026 at 21:39 UTC 2c732444a93f78eb5b45cee083db3c54f23159e7
3 files changed +14 -3
src/webdav.ts
+7 -3
@@ -218,8 +218,8 @@ export async function handledWebdav(ctx: Koa.Context) {
218 return `<?xml version="1.0" encoding="utf-8"?><prop xmlns="DAV:"><lockdiscovery><activelock>
219 <locktype><write/></locktype>
220 <lockscope><exclusive/></lockscope>
221 - <locktoken><href>${token}</href></locktoken>
222 - <lockroot><href>${path}</href></lockroot>
221 + <locktoken><href>${escapeXml(token)}</href></locktoken>
222 + <lockroot><href>${escapeXml(path)}</href></lockroot>
223 <depth>0</depth>
224 <timeout>Second-${seconds}</timeout>
225 </activelock></lockdiscovery></prop>`
@@ -259,7 +259,7 @@ export async function handledWebdav(ctx: Koa.Context) {
259 const isDir = await nodeIsFolder(node)
260 const st = await nodeStats(node)
261 res.write(`<response>
262 - <href>${outPath + (append ? pathEncode(name, true) + (isDir ? '/' : '') : '')}</href>
262 + <href>${escapeXml(outPath + (append ? pathEncode(name, true) + (isDir ? '/' : '') : ''))}</href>
263 <propstat>
264 <status>HTTP/1.1 200 OK</status>
265 <prop>
@@ -311,6 +311,10 @@ export async function handledWebdav(ctx: Koa.Context) {
311
312 }
313
314 +function escapeXml(value: string) {
315 + return value.replace(/[<>&'\"]/g, c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;', }[c] || c))
316 +}
317 +
318 function compileWebdavAgentRegex(v: boolean|string) {
319 return !v ? null : v === true ? /.*/ : new RegExp(v.trim(), 'i')
320 }
tests/config.yaml
+5
@@ -37,6 +37,11 @@ vfs:
37 source: ../page/gpl.png
38 can_read:
39 - rejetto
40 + - name: hidden
41 + can_see:
42 + this: false
43 + children:
44 + - name: a<&>;#'z
45 - name: for-admins
46 can_read:
47 - admins
tests/test.ts
+2
@@ -12,6 +12,7 @@ import { httpStream, parseHttpUrl, stream2string, XRequestOptions } from '../src
12 import { ThrottledStream, ThrottleGroup } from '../src/ThrottledStream'
13 import { mkdir, rm, rename, writeFile, access } from 'fs/promises'
14 import { Readable } from 'stream'
15 +import { XMLValidator } from 'fast-xml-parser'
16 /*
17 import { PORT, srv } from '../src'
18
@@ -343,6 +344,7 @@ describe('webdav', () => {
344 await rmAny(destPath)
345 }
346 })
347 + test('webdav.escaping', req('/f1/hidden', data => XMLValidator.validate(data) === true, { method: 'PROPFIND', auth, jar: {}, headers: { depth: '1' } }))
348
349 function webdavUpload(uri: string, tester: Tester, body: string, userAgent=WEBDAV_UA) {
350 return () => req(uri, tester, {