Files
rclone/backend/local/metadata.go
T
Nick Craig-Wood 215e6dbc65 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.
2026-09-04 19:00:22 +01:00

172 lines
4.2 KiB
Go

package local
import (
"fmt"
"math"
"os"
"runtime"
"strconv"
"time"
"github.com/rclone/rclone/fs"
)
const metadataTimeFormat = time.RFC3339Nano
// system metadata keys which this backend owns
//
// not all values supported on all OSes
var systemMetadataInfo = map[string]fs.MetadataHelp{
"mode": {
Help: "File type and mode",
Type: "octal, unix style",
Example: "0100664",
},
"uid": {
Help: "User ID of owner",
Type: "decimal number",
Example: "500",
},
"gid": {
Help: "Group ID of owner",
Type: "decimal number",
Example: "500",
},
"rdev": {
Help: "Device ID (if special file)",
Type: "hexadecimal",
Example: "1abc",
},
"atime": {
Help: "Time of last access",
Type: "RFC 3339",
Example: "2006-01-02T15:04:05.999999999Z07:00",
},
"mtime": {
Help: "Time of last modification",
Type: "RFC 3339",
Example: "2006-01-02T15:04:05.999999999Z07:00",
},
"btime": {
Help: "Time of file birth (creation)",
Type: "RFC 3339",
Example: "2006-01-02T15:04:05.999999999Z07:00",
},
}
// parse a time string from metadata with key
func (o *Object) parseMetadataTime(m fs.Metadata, key string) (t time.Time, ok bool) {
value, ok := m[key]
if ok {
var err error
t, err = time.Parse(metadataTimeFormat, value)
if err != nil {
fs.Debugf(o, "failed to parse metadata %s: %q: %v", key, value, err)
ok = false
}
}
return t, ok
}
// parse am int from metadata with key and base
func (o *Object) parseMetadataInt(m fs.Metadata, key string, base int) (result int, ok bool) {
value, ok := m[key]
if ok {
var err error
parsed, err := strconv.ParseInt(value, base, 0)
if err != nil {
fs.Debugf(o, "failed to parse metadata %s: %q: %v", key, value, err)
ok = false
}
result = int(parsed)
}
return result, ok
}
// Write the metadata into the file
//
// It isn't possible to set the ctime and btime under Unix
func (o *Object) writeMetadataToFile(m fs.Metadata) (outErr error) {
var err error
atime, atimeOK := o.parseMetadataTime(m, "atime")
mtime, mtimeOK := o.parseMetadataTime(m, "mtime")
btime, btimeOK := o.parseMetadataTime(m, "btime")
if atimeOK || mtimeOK {
if atimeOK && !mtimeOK {
mtime = atime
}
if !atimeOK && mtimeOK {
atime = mtime
}
err = o.setTimes(atime, mtime)
if err != nil {
outErr = fmt.Errorf("failed to set times: %w", err)
}
}
if haveSetBTime {
if btimeOK {
// 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)
}
if err != nil {
outErr = fmt.Errorf("failed to set birth (creation) time: %w", err)
}
}
}
uid, hasUID := o.parseMetadataInt(m, "uid", 10)
gid, hasGID := o.parseMetadataInt(m, "gid", 10)
if hasUID {
// FIXME should read UID and GID of current user and only attempt to set it if different
if !hasGID {
gid = uid
}
if runtime.GOOS == "windows" || runtime.GOOS == "plan9" {
fs.Debugf(o, "Ignoring request to set ownership %o.%o on this OS", gid, uid)
} else {
if o.translatedLink {
err = os.Lchown(o.path, uid, gid)
} else {
err = o.fs.chown(o.path, uid, gid)
}
if err != nil {
outErr = fmt.Errorf("failed to change ownership: %w", err)
}
}
}
mode, hasMode := o.parseMetadataInt(m, "mode", 8)
if hasMode {
if mode >= 0 {
umode := uint(mode)
if umode <= math.MaxUint32 {
fileMode := os.FileMode(umode)
// fileMode comes from the source, which may be untrusted, so
// by default apply only the permission bits and strip the
// setuid, setgid and sticky bits.
if !o.fs.opt.MetadataRestoreSpecial {
fileMode = fileMode.Perm()
}
if o.translatedLink {
if haveLChmod {
err = lChmod(o.path, fileMode)
} else {
fs.Debugf(o, "Unable to set mode %v on a symlink on this OS", fileMode)
err = nil
}
} else {
err = o.fs.chmod(o.path, fileMode)
}
if err != nil {
outErr = fmt.Errorf("failed to change permissions: %w", err)
}
}
}
}
// FIXME not parsing rdev yet
return outErr
}