diff --git a/backend/local/local.go b/backend/local/local.go index a7c1eb05a..8e4a6afd2 100644 --- a/backend/local/local.go +++ b/backend/local/local.go @@ -1318,7 +1318,7 @@ func (o *Object) setTimes(atime, mtime time.Time) (err error) { if o.translatedLink { err = lChtimes(o.path, atime, mtime) } else { - err = os.Chtimes(o.path, atime, mtime) + err = o.fs.chtimes(o.path, atime, mtime) } return err } @@ -1568,6 +1568,45 @@ func (f *Fs) symlink(target, localPath string) (err error) { return root.Symlink(target, rel) } +// chmod changes the mode of localPath. +func (f *Fs) chmod(localPath string, mode os.FileMode) (err error) { + if !f.opt.TranslateSymlinks { + return os.Chmod(localPath, mode) + } + root, rel, err := f.osRoot(localPath) + if err != nil { + return err + } + defer fs.CheckClose(root, &err) + return root.Chmod(rel, mode) +} + +// chown changes the ownership of localPath. +func (f *Fs) chown(localPath string, uid, gid int) (err error) { + if !f.opt.TranslateSymlinks { + return os.Chown(localPath, uid, gid) + } + root, rel, err := f.osRoot(localPath) + if err != nil { + return err + } + defer fs.CheckClose(root, &err) + return root.Chown(rel, uid, gid) +} + +// chtimes changes the atime and mtime of localPath. +func (f *Fs) chtimes(localPath string, atime, mtime time.Time) (err error) { + if !f.opt.TranslateSymlinks { + return os.Chtimes(localPath, atime, mtime) + } + root, rel, err := f.osRoot(localPath) + if err != nil { + return err + } + defer fs.CheckClose(root, &err) + return root.Chtimes(rel, atime, mtime) +} + // mkdirAll makes all the directories needed to store the object func (o *Object) mkdirAll() error { return o.fs.mkdirAll(filepath.Dir(o.path)) diff --git a/backend/local/local_internal_test.go b/backend/local/local_internal_test.go index b1d8d047c..20e244df9 100644 --- a/backend/local/local_internal_test.go +++ b/backend/local/local_internal_test.go @@ -356,6 +356,97 @@ func TestSymlinkInTreeWriteThroughWorks(t *testing.T) { require.Equal(t, "world", string(got)) } +// TestDirMetadataThroughPlantedSymlinkBlocked checks metadata + +// symlinks can't write outside the root. +func TestDirMetadataThroughPlantedSymlinkBlocked(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlinks and unix modes not applicable on Windows") + } + ctx := context.Background() + + // A directory outside the destination whose metadata the attacker targets. + evil := t.TempDir() + evilDir := filepath.Join(evil, "secret.d") + require.NoError(t, os.Mkdir(evilDir, 0700)) + evilMtime := fstest.Time("2016-06-07T08:09:10Z") + require.NoError(t, os.Chtimes(evilDir, evilMtime, evilMtime)) + + r := fstest.NewRun(t) + f := r.Flocal.(*Fs) + linksMode(f) + + // The planted symlink dst/pwn -> outside, faithfully reproduced by --links. + require.NoError(t, putLink(ctx, f, "pwn", evilDir)) + + // The source now presents "pwn" as a directory with attacker-chosen mode + // and mtime. Applying it must not reach through the planted symlink. + metadata := fs.Metadata{ + "mode": "0777", + "mtime": "2001-02-03T04:05:06Z", + } + _, err := f.MkdirMetadata(ctx, "pwn", metadata) + require.Error(t, err, "applying metadata through a planted symlink should be refused") + + // The outside directory's mode and mtime must be unchanged. + fi, err := os.Stat(evilDir) + require.NoError(t, err) + require.Equal(t, os.FileMode(0700), fi.Mode().Perm(), "chmod escaped through planted symlink to %q", evilDir) + require.True(t, fi.ModTime().Equal(evilMtime), "chtimes escaped through planted symlink to %q", evilDir) +} + +// TestDirSetModTimeThroughPlantedSymlinkBlocked checks we can't +// chtimes outside the root with --links +func TestDirSetModTimeThroughPlantedSymlinkBlocked(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlinks not applicable on Windows") + } + ctx := context.Background() + + evil := t.TempDir() + evilDir := filepath.Join(evil, "secret.d") + require.NoError(t, os.Mkdir(evilDir, 0700)) + evilMtime := fstest.Time("2016-06-07T08:09:10Z") + require.NoError(t, os.Chtimes(evilDir, evilMtime, evilMtime)) + + r := fstest.NewRun(t) + f := r.Flocal.(*Fs) + linksMode(f) + + require.NoError(t, putLink(ctx, f, "pwn", evilDir)) + + err := f.DirSetModTime(ctx, "pwn", fstest.Time("2001-02-03T04:05:06Z")) + require.Error(t, err, "setting dir modtime through a planted symlink should be refused") + + fi, err := os.Stat(evilDir) + require.NoError(t, err) + require.True(t, fi.ModTime().Equal(evilMtime), "chtimes escaped through planted symlink to %q", evilDir) +} + +// TestDirMetadataInTreeWorks checks the root confinement doesn't +// break legitimate dir metadata. +func TestDirMetadataInTreeWorks(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("unix modes not applicable on Windows") + } + ctx := context.Background() + + r := fstest.NewRun(t) + f := r.Flocal.(*Fs) + linksMode(f) + + metadata := fs.Metadata{ + "mode": "0705", + "mtime": "2001-02-03T04:05:06Z", + } + _, err := f.MkdirMetadata(ctx, "sub", metadata) + require.NoError(t, err) + + fi, err := os.Stat(filepath.Join(f.root, "sub")) + require.NoError(t, err) + require.Equal(t, os.FileMode(0705), fi.Mode().Perm()) + require.True(t, fi.ModTime().Equal(fstest.Time("2001-02-03T04:05:06Z"))) +} + // TestEncodingEscapeBlocked checks that a name from a malicious source can't // be decoded into path syntax which writes outside the destination. func TestEncodingEscapeBlocked(t *testing.T) { diff --git a/backend/local/metadata.go b/backend/local/metadata.go index 1e04b5cfb..31d298767 100644 --- a/backend/local/metadata.go +++ b/backend/local/metadata.go @@ -128,7 +128,7 @@ func (o *Object) writeMetadataToFile(m fs.Metadata) (outErr error) { if o.translatedLink { err = os.Lchown(o.path, uid, gid) } else { - err = os.Chown(o.path, uid, gid) + err = o.fs.chown(o.path, uid, gid) } if err != nil { outErr = fmt.Errorf("failed to change ownership: %w", err) @@ -155,7 +155,7 @@ func (o *Object) writeMetadataToFile(m fs.Metadata) (outErr error) { err = nil } } else { - err = os.Chmod(o.path, fileMode) + err = o.fs.chmod(o.path, fileMode) } if err != nil { outErr = fmt.Errorf("failed to change permissions: %w", err)