S3 object keys are opaque names that may legally contain `..` segments. `serve s3` built backend paths with `path.Join(bucket, key)`, which normalised the key so a request such as `GET /bucket/../root-secret.txt` resolved to a file outside the selected bucket elsewhere under the serve root. Listing prefixes and multipart uploads were affected also. This did not allow reading of files outside the root, but did allow reading of files in the root which normally aren't visible; only directories are visible as buckets normally. Because `serve s3` maps keys to file paths it cannot represent every opaque S3 key, so rather than normalising keys (which would alias distinct keys onto one file as well as allow traversal) it now rejects any key that is not already in canonical path form - containing `..`, `.`, `//` or a leading or trailing slash - with a 400 Bad Request, as MinIO does. Directory listing prefixes are validated the same way but allow the empty bucket-root prefix and an optional trailing slash. Fixes: GHSA-8v25-v8p6-qf7v
56 lines
1.3 KiB
Go
56 lines
1.3 KiB
Go
package s3
|
|
|
|
import (
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/rclone/gofakes3"
|
|
"github.com/rclone/rclone/vfs"
|
|
)
|
|
|
|
func (b *s3Backend) entryListR(_vfs *vfs.VFS, bucketName, fdPath, name string, addPrefix bool, response *gofakes3.ObjectList) error {
|
|
fp, err := bucketDirPath(bucketName, fdPath)
|
|
if err != nil {
|
|
// A listing prefix that can't be represented as a path matches nothing.
|
|
return gofakes3.ErrNoSuchKey
|
|
}
|
|
|
|
dirEntries, err := getDirEntries(fp, _vfs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, entry := range dirEntries {
|
|
object := entry.Name()
|
|
|
|
// workaround for control-chars detect
|
|
objectPath := path.Join(fdPath, object)
|
|
|
|
if !strings.HasPrefix(object, name) {
|
|
continue
|
|
}
|
|
|
|
if entry.IsDir() {
|
|
if addPrefix {
|
|
prefixWithTrailingSlash := objectPath + "/"
|
|
response.AddPrefix(prefixWithTrailingSlash)
|
|
continue
|
|
}
|
|
err := b.entryListR(_vfs, bucketName, path.Join(fdPath, object), "", false, response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
item := &gofakes3.Content{
|
|
Key: objectPath,
|
|
LastModified: gofakes3.NewContentTime(entry.ModTime()),
|
|
ETag: getFileHash(entry, b.s.etagHashType),
|
|
Size: entry.Size(),
|
|
StorageClass: gofakes3.StorageStandard,
|
|
}
|
|
response.Add(item)
|
|
}
|
|
}
|
|
return nil
|
|
}
|