webdav: avoid poisoning of the UA-based logic
Massimo Melina committed
Mar 9, 2026 at 15:53 UTC
9a1c44cba7675240719a2e3ac017e6ccb6e3bcf2
1 file changed
+12
-9
src/webdav.ts
+12
-9
@@ -25,7 +25,7 @@ import _ from 'lodash'
25
const forceWebdavLogin = defineConfig<boolean|string, null|RegExp>(CFG.force_webdav_login, true, compileWebdavAgentRegex)
26
const webdavInitialAuth = defineConfig<boolean|string, null|RegExp>(CFG.webdav_initial_auth, 'WebDAVFS', compileWebdavAgentRegex)
27
const webdavPrompted = expiringCache<boolean>(DAY)
28
-const webdavDetectedAgents = new Set<string>()
28
+const webdavDetectedAgents = expiringCache<boolean>(DAY)
29
30
const TOKEN_HEADER = 'lock-token'
31
const WEBDAV_METHODS = new Set(['PROPFIND', 'MKCOL', 'MOVE', 'LOCK', 'UNLOCK'])
@@ -56,19 +56,17 @@ function hasToken(header: string, token: string) {
56
57
export async function handledWebdav(ctx: Koa.Context) {
58
let {path} = ctx
59
- path = path.replace(/^\/+/, '/') // double-slash is causing empty listing in filezilla pro
59
+ path = path.replace(/^\/+/, '/') // double-slash is causing empty listing in filezilla-pro
60
61
- const isWebdavAuthRequest = WEBDAV_METHODS.has(ctx.method) || WEBDAV_HINT_HEADERS.some(h => !!ctx.get(h))
61
const ua = ctx.get('user-agent')
63
- if (isWebdavAuthRequest && getCurrentUsername(ctx)) {
64
- if (ua)
65
- webdavDetectedAgents.add(ua)
66
- }
67
-
62
if (path.includes('/._') && ua?.startsWith('WebDAVFS')) {// too much spam from Finder for these files that can contain metas
63
ctx.state.dontLog = true
64
return ctx.status = HTTP_FORBIDDEN
65
}
66
+ const isWebdavAuthRequest = WEBDAV_METHODS.has(ctx.method) || WEBDAV_HINT_HEADERS.some(h => ctx.get(h))
67
+ if (isWebdavAuthRequest && ua && getCurrentUsername(ctx))
68
+ webdavDetectedAgents.try(webdavAgentKey(ctx, ua), () => true)
69
+
70
if (ctx.method === 'OPTIONS') {
71
if (ctx.get('Access-Control-Request-Method')) return // it's a preflight cors request, not webdav
72
setWebdavHeaders()
@@ -94,7 +92,7 @@ export async function handledWebdav(ctx: Koa.Context) {
92
if (x && ctx.length === undefined) // missing length can make PUT fail
93
ctx.req.headers['content-length'] = x
94
97
- if (KNOWN_UA.test(ua) || webdavDetectedAgents.has(ua))
95
+ if (KNOWN_UA.test(ua) || webdavDetectedAgents.has(webdavAgentKey(ctx, ua)))
96
ctx.query.existing ??= 'overwrite' // with webdav this is our default
97
return // default handling
98
}
@@ -317,6 +315,11 @@ function compileWebdavAgentRegex(v: boolean|string) {
315
return !v ? null : v === true ? /.*/ : new RegExp(v.trim(), 'i')
316
}
317
318
+function webdavAgentKey(ctx: Koa.Context, ua: string) {
319
+ // tying detection to source IP avoids promoting one spoofed UA to global WebDAV behavior
320
+ return `${ctx.ip}|${ua}`
321
+}
322
+
323
// Finder will upload special attributes as files with name ._* that can be merged using system utility "dot_clean"
324
const cleaners: Dict<Timeout> = {}
325
function dotClean(path: string) {