local: fix btime escaping the root via a planted symlink GHSA-f8g7-2xjc-7mfh CVE-PENDING

The birth-time (btime) write in writeMetadataToFile followed symlinks for
any object that was not a translated link, so under -l/--links a symlink
planted by an untrusted source at the destination path could redirect the
btime write to a target outside the backup destination on OSes where
birth time is settable (Windows).

Use the NOFOLLOW birth-time write whenever translating symlinks, not only
for translated links. It is a no-op on a real file or directory and stops
a planted symlink from being followed out of the destination.
This commit is contained in:
Nick Craig-Wood
2026-09-04 19:00:22 +01:00
parent 29bb1e1134
commit 215e6dbc65
2 changed files with 40 additions and 1 deletions
+36
View File
@@ -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) {
+4 -1
View File
@@ -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)