Files
ThothII/tools/thothctl/internal/safeio/files.go
T

64 lines
1.7 KiB
Go

// Package safeio reads installation files without following symlinked path components.
package safeio
import (
"errors"
"io"
"os"
"path/filepath"
"strings"
)
var ErrUnsafeFile = errors.New("unsafe file")
// ValidateCanonicalPath rejects relative or lexically non-canonical paths before they are opened.
func ValidateCanonicalPath(path string) error {
if !filepath.IsAbs(path) || filepath.Clean(path) != path || strings.Contains(path, string(filepath.Separator)+".."+string(filepath.Separator)) {
return ErrUnsafeFile
}
return nil
}
// ReadCanonicalRegular opens a canonical regular file after rejecting symlinked parents, then
// bounds reads against the opened handle rather than a pre-open size check.
func ReadCanonicalRegular(path string, maximum int64) ([]byte, error) {
if err := ValidateCanonicalPath(path); err != nil {
return nil, err
}
if err := rejectSymlinkComponents(path); err != nil {
return nil, err
}
file, err := os.Open(path)
if err != nil {
return nil, ErrUnsafeFile
}
defer file.Close()
info, err := file.Stat()
if err != nil || !info.Mode().IsRegular() {
return nil, ErrUnsafeFile
}
contents, err := io.ReadAll(io.LimitReader(file, maximum+1))
if err != nil || int64(len(contents)) > maximum {
return nil, ErrUnsafeFile
}
return contents, nil
}
func rejectSymlinkComponents(path string) error {
volume := filepath.VolumeName(path)
current := volume + string(filepath.Separator)
relative := strings.TrimPrefix(path, current)
for _, component := range strings.Split(relative, string(filepath.Separator)) {
if component == "" {
continue
}
current = filepath.Join(current, component)
info, err := os.Lstat(current)
if err != nil || info.Mode()&os.ModeSymlink != 0 {
return ErrUnsafeFile
}
}
return nil
}