diff --git a/backend/local/local_internal_test.go b/backend/local/local_internal_test.go index 20e244df9..82612be07 100644 --- a/backend/local/local_internal_test.go +++ b/backend/local/local_internal_test.go @@ -447,6 +447,42 @@ func TestDirMetadataInTreeWorks(t *testing.T) { require.True(t, fi.ModTime().Equal(fstest.Time("2001-02-03T04:05:06Z"))) } +// TestDirBTimeThroughPlantedSymlinkBlocked checks a btime write +// through a symlink can't escape the root. +func TestDirBTimeThroughPlantedSymlinkBlocked(t *testing.T) { + if !haveSetBTime { + t.Skip("birth time is not settable on this OS") + } + ctx := context.Background() + + evil := t.TempDir() + evilDir := filepath.Join(evil, "secret.d") + require.NoError(t, os.Mkdir(evilDir, 0700)) + + // Read the outside dir's btime through a local Fs rooted at evil. + evilFsRaw, err := NewFs(ctx, "local", evil, configmap.Simple{}) + require.NoError(t, err) + evilFs := evilFsRaw.(*Fs) + readBTime := func() string { + o, err := evilFs.newObject("secret.d") + require.NoError(t, err) + require.NoError(t, o.lstat()) + m, err := o.Metadata(ctx) + require.NoError(t, err) + return m["btime"] + } + before := readBTime() + + r := fstest.NewRun(t) + f := r.Flocal.(*Fs) + linksMode(f) + require.NoError(t, putLink(ctx, f, "pwn", evilDir)) + + _, _ = f.MkdirMetadata(ctx, "pwn", fs.Metadata{"btime": "2001-02-03T04:05:06Z"}) + + require.Equal(t, before, readBTime(), "btime escaped through planted symlink to %q", evilDir) +} + // 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 31d298767..bdbcad1f9 100644 --- a/backend/local/metadata.go +++ b/backend/local/metadata.go @@ -105,7 +105,10 @@ func (o *Object) writeMetadataToFile(m fs.Metadata) (outErr error) { } if haveSetBTime { if btimeOK { - if o.translatedLink { + // When translating symlinks, never follow the path. A planted symlink must + // not redirect the birth-time write out of the root. The NOFOLLOW open is a + // no-op on a real file or directory + if o.translatedLink || o.fs.opt.TranslateSymlinks { err = lsetBTime(o.path, btime) } else { err = setBTime(o.path, btime)