Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 03:30 UTC
fileserver: stat symlink targets by decoded path in browse listings (#8152)
directoryListing built each symlink's lookup path from the escaped request path. In a directory whose name needs escaping, such as one with a space, the stat failed, so directory symlinks were listed as files, file symlinks showed the link's own size, and reveal_symlinks showed no target. Use the decoded path, which the function already computes for the page title. The escaped path is still used for the page path and item URLs.
awss committed
Oct 5, 2026 at 23:42 UTC
96eadf18f6a694ef4650bd442f221c47c59fb857
2 files changed
+42
-3
modules/caddyhttp/fileserver/browsetplcontext.go
+4
-3
@@ -37,10 +37,11 @@ import (
37
func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, parentModTime time.Time, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext {
38
filesToHide := fsrv.transformHidePaths(repl)
39
40
- name, _ := url.PathUnescape(urlPath)
40
+ // urlPath is escaped, but filesystem lookups need the decoded path
41
+ reqPath, _ := url.PathUnescape(urlPath)
42
43
tplCtx := &browseTemplateContext{
43
- Name: path.Base(name),
44
+ Name: path.Base(reqPath),
45
Path: urlPath,
46
CanGoUp: canGoUp,
47
lastModified: parentModTime,
@@ -82,7 +83,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
83
var targetInfo fs.FileInfo
84
var targetPath string
85
if fileIsSymlink {
85
- targetPath = caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, info.Name()))
86
+ targetPath = caddyhttp.SanitizedPathJoin(root, path.Join(reqPath, info.Name()))
87
// An error most likely means the symlink target doesn't exist,
88
// which isn't entirely unusual and shouldn't fail the listing.
89
// In this case, targetInfo stays nil and we fall back to
modules/caddyhttp/fileserver/browsetplcontext_test.go
+38
@@ -15,7 +15,13 @@
15
package fileserver
16
17
import (
18
+ "context"
19
+ "os"
20
+ "path/filepath"
21
"testing"
22
+ "time"
23
+
24
+ "github.com/caddyserver/caddy/v2"
25
)
26
27
func TestBreadcrumbs(t *testing.T) {
@@ -104,3 +110,35 @@ func TestBreadcrumbs(t *testing.T) {
110
}
111
}
112
}
113
+
114
+func TestDirectoryListingSymlinkInEscapedPath(t *testing.T) {
115
+ root := t.TempDir()
116
+ target := filepath.Join(root, "target")
117
+ if err := os.Mkdir(target, 0o755); err != nil {
118
+ t.Fatal(err)
119
+ }
120
+
121
+ for i, tc := range []struct {
122
+ dir string
123
+ urlPath string // escaped, as serveBrowse passes it
124
+ }{
125
+ {dir: "plain", urlPath: "/plain"},
126
+ {dir: "with space", urlPath: "/with%20space"},
127
+ {dir: "ünïcode", urlPath: "/%C3%BCn%C3%AFcode"},
128
+ {dir: "100%", urlPath: "/100%25"},
129
+ } {
130
+ dir := filepath.Join(root, tc.dir)
131
+ if err := os.Mkdir(dir, 0o755); err != nil {
132
+ t.Fatal(err)
133
+ }
134
+ if err := os.Symlink(target, filepath.Join(dir, "link")); err != nil {
135
+ t.Skipf("symlink not supported on this platform: %v", err)
136
+ }
137
+
138
+ fileSystem, entries := readBenchDirEntries(t, dir)
139
+ listing := benchFileServer().directoryListing(context.Background(), fileSystem, time.Time{}, entries, true, root, tc.urlPath, caddy.NewReplacer())
140
+ if len(listing.Items) != 1 || !listing.Items[0].IsDir {
141
+ t.Errorf("Test %d: expected the symlink in %q to be listed as a directory", i, tc.dir)
142
+ }
143
+ }
144
+}