mirror of
https://github.com/rclone/rclone.git
synced 2026-02-21 03:43:32 +00:00
archive: extract: strip "./" prefix from tar entry paths
Tar files created from the current directory (e.g. tar -czf archive.tar.gz .) produce entries prefixed with "./". When extracting, rclone's character encoding replaces the "." with a full-width dot (U+FF0E), creating a spurious directory instead of merging into the destination root. Strip the leading "./" from NameInArchive before processing. Only "./" is stripped specifically to avoid enabling path traversal attacks via "../". Fixes #9168
This commit is contained in:
@@ -147,6 +147,16 @@ func ArchiveExtract(ctx context.Context, dst fs.Fs, dstDir string, src fs.Fs, sr
|
||||
// extract files
|
||||
err = ex.Extract(ctx, in, func(ctx context.Context, f archives.FileInfo) error {
|
||||
remote := f.NameInArchive
|
||||
// Strip leading "./" from archive paths. Tar files created with
|
||||
// relative paths (e.g. "tar -czf archive.tar.gz .") use "./" prefixed
|
||||
// entries. Without stripping, rclone encodes the "." as a full-width
|
||||
// dot character creating a spurious directory. We only strip "./"
|
||||
// specifically to avoid enabling path traversal attacks via "../".
|
||||
remote = strings.TrimPrefix(remote, "./")
|
||||
// If the entry was exactly "./" (the root dir), skip it
|
||||
if remote == "" && f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if dstDir != "" {
|
||||
remote = path.Join(dstDir, remote)
|
||||
}
|
||||
|
||||
62
cmd/archive/extract/extract_test.go
Normal file
62
cmd/archive/extract/extract_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
//go:build !plan9
|
||||
|
||||
package extract
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStripDotSlashPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "strip leading dot-slash from file",
|
||||
input: "./file.txt",
|
||||
expected: "file.txt",
|
||||
},
|
||||
{
|
||||
name: "strip leading dot-slash from nested path",
|
||||
input: "./subdir/file.txt",
|
||||
expected: "subdir/file.txt",
|
||||
},
|
||||
{
|
||||
name: "no prefix unchanged",
|
||||
input: "file.txt",
|
||||
expected: "file.txt",
|
||||
},
|
||||
{
|
||||
name: "nested path unchanged",
|
||||
input: "dir/file.txt",
|
||||
expected: "dir/file.txt",
|
||||
},
|
||||
{
|
||||
name: "dot-dot-slash NOT stripped (path traversal safety)",
|
||||
input: "../etc/passwd",
|
||||
expected: "../etc/passwd",
|
||||
},
|
||||
{
|
||||
name: "dot-slash directory entry becomes empty",
|
||||
input: "./",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "only single leading dot-slash stripped",
|
||||
input: "././file.txt",
|
||||
expected: "./file.txt",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// This mirrors the stripping logic in ArchiveExtract
|
||||
got := strings.TrimPrefix(tc.input, "./")
|
||||
assert.Equal(t, tc.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user