Files
rclone/lib/sanitize/sanitize_test.go
T
Nick Craig-Wood 2b7d0b16ed lib/sanitize: factor untrusted path sanitization out of archive extract
Move the archive entry name validation added for CVE-2026-59732 from
cmd/archive/extract into a new lib/sanitize package as sanitize.Path,
so the same check can be shared with the archive backend which mounts
archives as a filesystem.

sanitize.Path keeps the extract semantics - reject any name with a
".." path component, treating both "/" and "\" as separators - and
additionally cleans the name with path.Clean. This corrects two edge
cases in extract: a repeated "./" prefix ("././file.txt") is now fully
stripped rather than only the first, and a bare "." entry is now
treated as the archive root and skipped.

Add sanitize.Leaf, which rejects a name that is empty, ".", ".." or
contains a "/", for checking a single directory entry name read from
an archive.

The names handled are rclone remote paths, in which "/" is the only
separator and "\" an ordinary character, so Leaf does not reject a
backslash: making a name safe for its storage is the destination
backend's job (the local backend encodes "\" on Windows and refuses
paths which escape its root). Path's rejection of ".." between
backslashes is kept as defence in depth for extract.
2026-09-04 19:00:22 +01:00

170 lines
3.4 KiB
Go

package sanitize
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPath(t *testing.T) {
tests := []struct {
name string
input string
expected string
wantErr bool
}{
{
name: "plain file unchanged",
input: "file.txt",
expected: "file.txt",
},
{
name: "nested path unchanged",
input: "dir/file.txt",
expected: "dir/file.txt",
},
{
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: "strip repeated leading dot-slash",
input: "././file.txt",
expected: "file.txt",
},
{
name: "strip interior dot component",
input: "dir/./file.txt",
expected: "dir/file.txt",
},
{
name: "strip leading slash",
input: "/dir/file.txt",
expected: "dir/file.txt",
},
{
name: "strip trailing slash from directory",
input: "dir/",
expected: "dir",
},
{
name: "collapse doubled slashes",
input: "dir//file.txt",
expected: "dir/file.txt",
},
{
name: "empty name is the root",
input: "",
expected: "",
},
{
name: "dot-slash is the root",
input: "./",
expected: "",
},
{
name: "dot is the root",
input: ".",
expected: "",
},
{
name: "slash is the root",
input: "/",
expected: "",
},
{
name: "three dots allowed",
input: "dir/...",
expected: "dir/...",
},
{
name: "backslash kept in file name",
input: `dir/back\slash.txt`,
expected: `dir/back\slash.txt`,
},
{
name: "leading dot-dot rejected",
input: "../etc/passwd",
wantErr: true,
},
{
name: "interior dot-dot rejected",
input: "dir/../../escaped.txt",
wantErr: true,
},
{
name: "trailing dot-dot rejected",
input: "dir/..",
wantErr: true,
},
{
name: "bare dot-dot rejected",
input: "..",
wantErr: true,
},
{
name: "backslash dot-dot rejected",
input: `..\escaped.txt`,
wantErr: true,
},
{
name: "nested backslash dot-dot rejected",
input: `dir\..\..\escaped.txt`,
wantErr: true,
},
{
name: "mixed separator dot-dot rejected",
input: `dir/..\escaped.txt`,
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := Path(tc.input)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.expected, got)
})
}
}
func TestLeaf(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{name: "plain name", input: "file.txt"},
{name: "name with dots", input: "..."},
{name: "hidden name", input: ".hidden"},
{name: "empty rejected", input: "", wantErr: true},
{name: "dot rejected", input: ".", wantErr: true},
{name: "dot-dot rejected", input: "..", wantErr: true},
{name: "slash rejected", input: "a/b", wantErr: true},
{name: "backslash allowed", input: `a\b`},
{name: "leading slash rejected", input: "/etc", wantErr: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := Leaf(tc.input)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}