From 93ba2d40f094f3dcc49ede2e686ccde642dfed75 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 9 Sep 2026 15:45:45 +0100 Subject: [PATCH] vfs: stop --links objects called "...rclonelink" appearing as ".." in directory listings When reading a directory with --links the VFS skipped entries called "." and ".." before removing the ".rclonelink" suffix, so an object called "...rclonelink" (or "..rclonelink" or ".rclonelink") produced a directory entry called ".." (or "." or "") whose path resolved to the parent directory. The same happened for names added back from the VFS cache. The check is now done after the suffix is removed. See GHSA-55qp-jrwr-x827 --- vfs/dir.go | 11 ++++++++--- vfs/dir_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/vfs/dir.go b/vfs/dir.go index 243f0d321..e9cbbbc99 100644 --- a/vfs/dir.go +++ b/vfs/dir.go @@ -478,6 +478,9 @@ func (d *Dir) AddVirtual(leaf string, size int64, isDir bool) { // since the path came from the cache it may have fs.LinkSuffix, // so remove it and mark the *File accordingly leaf, isLink = strings.CutSuffix(leaf, fs.LinkSuffix) + if leaf == "" || leaf == "." || leaf == ".." { + return + } } f := newFile(d, dPath, nil, leaf) if isLink { @@ -724,12 +727,14 @@ func (d *Dir) _readDirFromEntries(entries fs.DirEntries, dirTree dirtree.DirTree mv := d._newManageVirtuals() for _, entry := range entries { name := path.Base(entry.Remote()) - if name == "." || name == ".." { - continue - } if d.vfs.Opt.Links { name, _ = strings.CutSuffix(name, fs.LinkSuffix) } + // Check after removing the suffix so "...rclonelink" can't + // become an entry called ".." + if name == "" || name == "." || name == ".." { + continue + } node := d.items[name] if mv.add(d, name) { continue diff --git a/vfs/dir_test.go b/vfs/dir_test.go index 264319d7f..0d5f2c8e2 100644 --- a/vfs/dir_test.go +++ b/vfs/dir_test.go @@ -16,6 +16,7 @@ import ( "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/operations" "github.com/rclone/rclone/fstest" + "github.com/rclone/rclone/vfs/vfscommon" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -33,6 +34,41 @@ func dirCreate(t *testing.T) (r *fstest.Run, vfs *VFS, dir *Dir, item fstest.Ite return r, vfs, node.(*Dir), file1 } +// TestDirLinksInvalidName checks that with --links an object called +// "...rclonelink" doesn't become a directory entry called "..", which +// would address the parent, whether it comes from a listing or from +// the cache. +func TestDirLinksInvalidName(t *testing.T) { + opt := vfscommon.Opt + opt.Links = true + r, vfs := newTestVFSOpt(t, &opt) + + file1 := r.WriteObject(context.Background(), "dir/.."+fs.LinkSuffix, "target", t1) + file2 := r.WriteObject(context.Background(), "dir/file1", "file1 contents", t1) + r.CheckRemoteItems(t, file1, file2) + + node, err := vfs.Stat("dir") + require.NoError(t, err) + dir := node.(*Dir) + + checkEntries := func() { + t.Helper() + entries, err := dir.ReadDirAll() + require.NoError(t, err) + var names []string + for _, entry := range entries { + names = append(names, entry.Name()) + } + assert.Equal(t, []string{"file1"}, names) + } + checkEntries() + + for _, leaf := range []string{".." + fs.LinkSuffix, "." + fs.LinkSuffix, fs.LinkSuffix} { + dir.AddVirtual(leaf, 6, false) + } + checkEntries() +} + func TestDirMethods(t *testing.T) { _, vfs, dir, _ := dirCreate(t)