Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tar/sanitize_other_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !windows

package tar

func invalidFileNames() []string {
return nil // No special cases for this platform.
}
12 changes: 9 additions & 3 deletions tar/sanitize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import (
)

func TestValidatePlatformPath(t *testing.T) {
// Expect error if path contains null
if err := validatePlatformPath("foo\x00bar"); err == nil {
t.Fatal("expected error")
for _, name := range append(
invalidFileNames(),
"foo\x00bar", // Expect error if path contains null
) {
t.Run(name, func(t *testing.T) {
if err := validatePlatformPath(name); err == nil {
t.Fatal("expected error")
}
})
}

// No specification for a path component containing a "." component
Expand Down
102 changes: 52 additions & 50 deletions tar/sanitize_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,75 @@ package tar

import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
)

// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
var reservedNames = [...]string{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}
// NOTE: `/` is not included, it is already reserved as the standard path separator.
const reservedRunes = `<>:"\|?*` + "\x00"

const reservedCharsStr = `[<>:"\|?*]` + "\x00" // NOTE: `/` is not included as it is our standard path separator
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
var reservedNames = [...]string{
"CON", "PRN", "AUX", "NUL", "COM1", "COM2",
"COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
"LPT6", "LPT7", "LPT8", "LPT9",
}

func isNullDevice(path string) bool {
// This is a case insensitive comparison to NUL
if len(path) != 3 {
return false
}
if path[0]|0x20 != 'n' {
return false
}
if path[1]|0x20 != 'u' {
return false
}
if path[2]|0x20 != 'l' {
return false
}
return true
// "NUL" is standard but the device is case insensitive.
// Normalize to upper for the compare.
const nameLen = len(os.DevNull)
return len(path) == nameLen && strings.ToUpper(path) == os.DevNull
}

// validatePathComponent returns an error if the given path component is not allowed on the platform
func validatePathComponent(c string) error {
// MSDN: Do not end a file or directory name with a space or a period
if strings.HasSuffix(c, ".") {
return fmt.Errorf("invalid platform path: path components cannot end with '.' : %q", c)
}
if strings.HasSuffix(c, " ") {
return fmt.Errorf("invalid platform path: path components cannot end with ' ' : %q", c)
}

if c == ".." {
return fmt.Errorf("invalid platform path: path component cannot be '..'")
// validatePathComponent returns an error if the given path component is not allowed on the platform.
func validatePathComponent(component string) error {
const invalidPathErr = "invalid platform path"
for _, suffix := range [...]string{
".", // MSDN: Do not end a file or directory
" ", // name with a space or a period.
} {
if strings.HasSuffix(component, suffix) {
return fmt.Errorf(
`%s: path components cannot end with '%s': "%s"`,
invalidPathErr, suffix, component,
)
}
}
// error on reserved characters
if strings.ContainsAny(c, reservedCharsStr) {
return fmt.Errorf("invalid platform path: path components cannot contain any of %s : %q", reservedCharsStr, c)
if strings.ContainsAny(component, reservedRunes) {
return fmt.Errorf(
`%s: path components cannot contain any of "%s": "%s"`,
invalidPathErr, reservedRunes, component,
)
}

// error on reserved names
for _, rn := range reservedNames {
if c == rn {
return fmt.Errorf("invalid platform path: path component is a reserved name: %s", c)
}
if slices.Contains(reservedNames[:], strings.ToUpper(component)) {
return fmt.Errorf(
`%s: path component is a reserved name: "%s"`,
invalidPathErr, component,
)
}

return nil
}

func validatePlatformPath(platformPath string) error {
// remove the volume name
p := platformPath[len(filepath.VolumeName(platformPath)):]

// convert to cleaned slash-path
p = filepath.ToSlash(p)
p = strings.Trim(p, "/")

// make sure all components of the path are valid
for _, e := range strings.Split(p, "/") {
if err := validatePathComponent(e); err != nil {
func validatePlatformPath(nativePath string) error {
normalized := normalizeToGoPath(nativePath)
for component := range strings.SplitSeq(normalized, "/") {
if err := validatePathComponent(component); err != nil {
return err
}
}
return nil
}

func normalizeToGoPath(nativePath string) string {
var (
volumeName = filepath.VolumeName(nativePath)
relativeNative = nativePath[len(volumeName):]
goPath = filepath.ToSlash(relativeNative)
relativeGo = strings.Trim(goPath, "/")
)
return relativeGo
}
16 changes: 16 additions & 0 deletions tar/sanitize_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build windows

package tar

func invalidFileNames() []string {
return []string{
"foo.", // Cannot end in '.'.
"foo ", // Cannot end in ' '.
"..", // Cannot be parent dir name.
"CON", // Cannot be device name.
"nul", // Cannot be device name (case insensitive).
"AuX", // Cannot be device name (case insensitive).
"foo?", // Cannot use reserved character '?'.
`<\f|o:o*>`, // Cannot use reserved characters (multiple).
}
}
Loading