1
0
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:
Varun Chawla
2026-02-20 03:46:53 -08:00
committed by GitHub
parent 9601dbce87
commit 5042f360f0
2 changed files with 72 additions and 0 deletions

View File

@@ -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)
}

View 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)
})
}
}