From fd8b28d36dd176844c48caf7ca5c2017ab5f8b47 Mon Sep 17 00:00:00 2001 From: Varun Chawla <34209028+veeceey@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:30:15 -0800 Subject: [PATCH] webdav: escape reserved characters in URL path segments Use URLPathEscapeAll instead of URLPathEscape for path encoding. URLPathEscape relies on Go's url.URL.String() which only minimally escapes paths - reserved sub-delimiter characters like semicolons and equals signs pass through unescaped. Per RFC 3986 section 3.3, these characters must be percent-encoded when used as literal values in path segments. Some WebDAV servers (notably dCache/Jetty) interpret unescaped semicolons as path parameter delimiters, which truncates filenames at the semicolon position. URLPathEscapeAll encodes everything except [A-Za-z0-9/], which is safe for all servers. Fixes #9082 --- backend/webdav/webdav.go | 2 +- backend/webdav/webdav_internal_test.go | 32 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/backend/webdav/webdav.go b/backend/webdav/webdav.go index bc6a0241f..09746f2b6 100644 --- a/backend/webdav/webdav.go +++ b/backend/webdav/webdav.go @@ -423,7 +423,7 @@ func (f *Fs) filePath(file string) string { if f.opt.Enc != encoder.EncodeZero { subPath = f.opt.Enc.FromStandardPath(subPath) } - return rest.URLPathEscape(subPath) + return rest.URLPathEscapeAll(subPath) } // dirPath returns a directory path (f.root, dir) diff --git a/backend/webdav/webdav_internal_test.go b/backend/webdav/webdav_internal_test.go index c2c2d1dfa..ee6cd7116 100644 --- a/backend/webdav/webdav_internal_test.go +++ b/backend/webdav/webdav_internal_test.go @@ -80,3 +80,35 @@ func TestHeaders(t *testing.T) { _, err := f.Features().About(context.Background()) require.NoError(t, err) } + +// TestReservedCharactersInPathAreEscaped verifies that reserved characters +// like semicolons and equals signs in file paths are percent-encoded in +// HTTP requests to the WebDAV server (RFC 3986 compliance). +func TestReservedCharactersInPathAreEscaped(t *testing.T) { + var capturedPath string + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedPath = r.RequestURI + // Return a 404 so the NewObject call fails cleanly + w.WriteHeader(http.StatusNotFound) + }) + ts := httptest.NewServer(handler) + defer ts.Close() + + configfile.Install() + m := configmap.Simple{ + "type": "webdav", + "url": ts.URL, + } + + f, err := webdav.NewFs(context.Background(), remoteName, "", m) + require.NoError(t, err) + + // Try to access a file with a semicolon in the name. + // We expect the request to fail (404), but the path should be escaped. + _, _ = f.NewObject(context.Background(), "my;test") + + // The semicolon must be percent-encoded as %3B + assert.Contains(t, capturedPath, "my%3Btest", "semicolons in path should be percent-encoded") + assert.NotContains(t, capturedPath, "my;test", "raw semicolons should not appear in path") +}