Harden Windows package paths (T-605)
This commit is contained in:
@@ -7,13 +7,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"softbox.local/core/domain"
|
||||
"softbox.local/core/internal/safepath"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -281,12 +280,13 @@ func (record InstalledApp) validate() error {
|
||||
|
||||
seenPaths := make(map[string]struct{}, len(record.Files))
|
||||
for index, installedFile := range record.Files {
|
||||
if !validInstalledPath(installedFile.Path) {
|
||||
if err := safepath.ValidateRelative(installedFile.Path); err != nil {
|
||||
return fmt.Errorf(
|
||||
"%w: files[%d].path=%q",
|
||||
"%w: files[%d].path=%q: %v",
|
||||
ErrInstalledAppInvalid,
|
||||
index,
|
||||
installedFile.Path,
|
||||
err,
|
||||
)
|
||||
}
|
||||
if installedFile.Size < 0 {
|
||||
@@ -304,7 +304,7 @@ func (record InstalledApp) validate() error {
|
||||
index,
|
||||
)
|
||||
}
|
||||
foldedPath := strings.ToLower(installedFile.Path)
|
||||
foldedPath := safepath.CollisionKey(installedFile.Path)
|
||||
if _, exists := seenPaths[foldedPath]; exists {
|
||||
return fmt.Errorf(
|
||||
"%w: duplicate file path %q",
|
||||
@@ -317,18 +317,6 @@ func (record InstalledApp) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validInstalledPath(value string) bool {
|
||||
if value == "" || strings.Contains(value, `\`) || strings.Contains(value, ":") {
|
||||
return false
|
||||
}
|
||||
cleaned := path.Clean(value)
|
||||
return cleaned == value &&
|
||||
cleaned != "." &&
|
||||
!strings.HasPrefix(cleaned, "/") &&
|
||||
cleaned != ".." &&
|
||||
!strings.HasPrefix(cleaned, "../")
|
||||
}
|
||||
|
||||
func requireRealDirectory(directory string) error {
|
||||
info, err := os.Lstat(directory)
|
||||
if err != nil {
|
||||
|
||||
@@ -74,6 +74,42 @@ func TestInstalledAppStoreRejectsInvalidRecords(t *testing.T) {
|
||||
record.Files[0].Path = "../escape.exe"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Windows normalized escape",
|
||||
mutate: func(record *InstalledApp) {
|
||||
record.Files[0].Path = ".. /escape.exe"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trailing period",
|
||||
mutate: func(record *InstalledApp) {
|
||||
record.Files[0].Path = "JsonParser.exe."
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trailing space",
|
||||
mutate: func(record *InstalledApp) {
|
||||
record.Files[0].Path = "JsonParser.exe "
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "leading space",
|
||||
mutate: func(record *InstalledApp) {
|
||||
record.Files[0].Path = " JsonParser.exe"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reserved device",
|
||||
mutate: func(record *InstalledApp) {
|
||||
record.Files[0].Path = "NUL.txt"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "forbidden Windows character",
|
||||
mutate: func(record *InstalledApp) {
|
||||
record.Files[0].Path = "Json?Parser.exe"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid hash",
|
||||
mutate: func(record *InstalledApp) {
|
||||
@@ -103,6 +139,22 @@ func TestInstalledAppStoreRejectsInvalidRecords(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstalledAppStoreAcceptsUnicodeNestedPath(t *testing.T) {
|
||||
record := validInstalledApp()
|
||||
record.Files[0].Path = "工具/解析器.exe"
|
||||
store := NewInstalledAppStore(filepath.Join(t.TempDir(), "apps"))
|
||||
if err := store.Write(record); err != nil {
|
||||
t.Fatalf("Write() error = %v", err)
|
||||
}
|
||||
loaded, found, err := store.Read(record.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Read() error = %v", err)
|
||||
}
|
||||
if !found || loaded.Files[0].Path != record.Files[0].Path {
|
||||
t.Fatalf("Read() = %#v, found=%t", loaded, found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstalledAppStoreRejectsUnknownFieldsAndIDMismatch(t *testing.T) {
|
||||
appsRoot := filepath.Join(t.TempDir(), "apps")
|
||||
appRoot := filepath.Join(appsRoot, "json-parser")
|
||||
|
||||
Reference in New Issue
Block a user