fs: confine directory listing entries that escape the root GHSA-3vxh-3pcx-9m8q GHSA-38xv-hf3p-h7mq CVE-PENDING
The rclone core does not sanitise ".." in an object's Remote(). Such a name can arrive from a malicious or buggy backend - an object store permits keys containing ".." or a leading "/" - and, if acted on, lets a listing or transfer escape the configured root. A source object named "../../other/x" is copied to "other/x" outside the destination root, and a crafted listing name surfaces outside the directory being listed. Add list.RemoteEscapesRoot, which reports whether a Remote climbs above the root when joined onto it, and list.RemoveEscaping, which drops and logs such entries. Apply RemoveEscaping unconditionally - independent of the include/exclude filters - at the three per-entry filtering points every listing passes through: filterDir, walk.listR and walk.walkRDirTree (recursive ListR). operations.StatJSON calls List and NewObject directly, bypassing those, so it rejects an escaping remote up front. This confines every backend at once, so no per-backend change is needed.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/accounting"
|
||||
"github.com/rclone/rclone/fs/hash"
|
||||
"github.com/rclone/rclone/fs/list"
|
||||
"github.com/rclone/rclone/fs/walk"
|
||||
)
|
||||
|
||||
@@ -277,6 +278,14 @@ func StatJSON(ctx context.Context, fsrc fs.Fs, remote string, opt *ListJSONOpt)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// A remote that climbs above the Fs root can never be a valid item.
|
||||
// StatJSON calls List/NewObject directly, bypassing the confinement in
|
||||
// fs/list and fs/walk, so treat it as not found here rather than stat
|
||||
// something outside the configured root.
|
||||
if list.RemoteEscapesRoot(remote) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Root is always a directory. When we have a NewDirEntry
|
||||
// primitive we need to call it, but for now this will do.
|
||||
if remote == "" {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/rclone/rclone/fs/accounting"
|
||||
"github.com/rclone/rclone/fs/operations"
|
||||
"github.com/rclone/rclone/fstest"
|
||||
"github.com/rclone/rclone/fstest/mockfs"
|
||||
"github.com/rclone/rclone/fstest/mockobject"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -462,3 +464,34 @@ func TestStatJSONMemory(t *testing.T) {
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
}
|
||||
|
||||
// TestStatJSONConfinement checks that StatJSON never returns an item whose
|
||||
// remote escapes the Fs root. StatJSON calls List/NewObject directly and so
|
||||
// bypasses the confinement in fs/list and fs/walk - the escaping objects added
|
||||
// below would be returned without the guard in StatJSON.
|
||||
func TestStatJSONConfinement(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ff, err := mockfs.NewFs(ctx, "mock", "/", nil)
|
||||
require.NoError(t, err)
|
||||
f := ff.(*mockfs.Fs)
|
||||
f.AddObject(mockobject.Object("ok"))
|
||||
f.AddObject(mockobject.Object(".."))
|
||||
f.AddObject(mockobject.Object("../evil"))
|
||||
f.AddObject(mockobject.Object("/../slashevil"))
|
||||
f.AddObject(mockobject.Object("//../../slashevil2"))
|
||||
f.AddObject(mockobject.Object("/../../etc/passwd"))
|
||||
|
||||
// Escaping remotes are treated as not found, including the leading-slash
|
||||
// variants that path.Clean would anchor as absolute and miss.
|
||||
for _, remote := range []string{"..", "../evil", "/../slashevil", "//../../slashevil2", "/../../etc/passwd"} {
|
||||
got, err := operations.StatJSON(ctx, f, remote, &operations.ListJSONOpt{})
|
||||
require.NoError(t, err, remote)
|
||||
assert.Nil(t, got, remote)
|
||||
}
|
||||
|
||||
// A legitimate remote is still returned.
|
||||
got, err := operations.StatJSON(ctx, f, "ok", &operations.ListJSONOpt{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, "ok", got.Path)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user