The zip archiver handed out its cached directory tree directly. Any caller which filters a listing in place (as the core listing code does) altered the cache, so later listings of the same directory could be corrupted. Return a copy of the cached listing instead.
158 lines
4.0 KiB
Go
158 lines
4.0 KiB
Go
package zip
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "github.com/rclone/rclone/backend/local"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/cache"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// writeZip builds a zip file with the given entry names (a name ending
|
|
// in "/" is a directory) in dir and returns its leaf name.
|
|
func writeZip(t *testing.T, dir, name string, names ...string) string {
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
for _, n := range names {
|
|
w, err := zw.Create(n)
|
|
require.NoError(t, err)
|
|
if !strings.HasSuffix(n, "/") {
|
|
_, err = w.Write([]byte("data for " + n))
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
require.NoError(t, zw.Close())
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, name), buf.Bytes(), 0600))
|
|
return name
|
|
}
|
|
|
|
// allRemotes returns every Object remote in the mounted archive's dirtree.
|
|
func allRemotes(t *testing.T, f fs.Fs) []string {
|
|
zf, ok := f.(*Fs)
|
|
require.True(t, ok)
|
|
var remotes []string
|
|
for _, entries := range zf.dt {
|
|
for _, entry := range entries {
|
|
if o, ok := entry.(*Object); ok {
|
|
remotes = append(remotes, o.Remote())
|
|
}
|
|
}
|
|
}
|
|
// dt iteration order is nondeterministic, so sort for stable comparison.
|
|
sort.Strings(remotes)
|
|
return remotes
|
|
}
|
|
|
|
// A malicious zip whose entry names escape the archive's own namespace
|
|
// must not be exposed by the zip backend (Zip Slip).
|
|
func TestReadZipSlip(t *testing.T) {
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
name := writeZip(t, dir, "evil.zip",
|
|
"good.txt",
|
|
"../../etc/cron.d/evil",
|
|
"../escape.txt",
|
|
"sub/../../above.txt",
|
|
`..\..\evil.exe`,
|
|
)
|
|
|
|
localFs, err := cache.Get(ctx, dir)
|
|
require.NoError(t, err)
|
|
|
|
f, err := New(ctx, localFs, name, "", "")
|
|
require.NoError(t, err)
|
|
|
|
remotes := allRemotes(t, f)
|
|
|
|
// The one benign entry must survive.
|
|
assert.Contains(t, remotes, "good.txt")
|
|
|
|
// No escaping entry may be exposed.
|
|
for _, remote := range remotes {
|
|
assert.False(t, remote == ".." || strings.HasPrefix(remote, "../"),
|
|
"escaping remote exposed: %q", remote)
|
|
}
|
|
assert.Equal(t, []string{"good.txt"}, remotes)
|
|
}
|
|
|
|
// Mounting with a non-empty root must only expose entries within that
|
|
// root directory, not sibling directories that merely share a name
|
|
// prefix (root "foo" must not match "foobar").
|
|
func TestReadZipRootBoundary(t *testing.T) {
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
name := writeZip(t, dir, "test.zip",
|
|
"foo/a.txt",
|
|
"foobar/b.txt",
|
|
)
|
|
|
|
localFs, err := cache.Get(ctx, dir)
|
|
require.NoError(t, err)
|
|
|
|
f, err := New(ctx, localFs, name, "", "foo")
|
|
require.NoError(t, err)
|
|
|
|
remotes := allRemotes(t, f)
|
|
assert.Equal(t, []string{"a.txt"}, remotes)
|
|
}
|
|
|
|
// A file entry whose name refers to the archive's own root (".", "/",
|
|
// "./" or "") must be skipped, not turn the whole archive into a single
|
|
// file which hides every other entry.
|
|
func TestReadZipRootNamedEntry(t *testing.T) {
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
name := writeZip(t, dir, "dot.zip",
|
|
".",
|
|
"/",
|
|
"./",
|
|
"",
|
|
"good.txt",
|
|
)
|
|
|
|
localFs, err := cache.Get(ctx, dir)
|
|
require.NoError(t, err)
|
|
|
|
f, err := New(ctx, localFs, name, "", "")
|
|
require.NoError(t, err)
|
|
|
|
remotes := allRemotes(t, f)
|
|
assert.Equal(t, []string{"good.txt"}, remotes)
|
|
}
|
|
|
|
// Listings are served from a cache which must survive callers
|
|
// filtering the returned slice in place, as fs/list does.
|
|
func TestListCacheNotAliased(t *testing.T) {
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
name := writeZip(t, dir, "test.zip", "a.txt", "b.txt", "c.txt")
|
|
|
|
localFs, err := cache.Get(ctx, dir)
|
|
require.NoError(t, err)
|
|
f, err := New(ctx, localFs, name, "", "")
|
|
require.NoError(t, err)
|
|
|
|
entries, err := f.List(ctx, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 3)
|
|
// Compact in place, dropping the first entry
|
|
copy(entries, entries[1:])
|
|
|
|
entries, err = f.List(ctx, "")
|
|
require.NoError(t, err)
|
|
var remotes []string
|
|
for _, entry := range entries {
|
|
remotes = append(remotes, entry.Remote())
|
|
}
|
|
assert.Equal(t, []string{"a.txt", "b.txt", "c.txt"}, remotes)
|
|
}
|