64 lines
1.7 KiB
Go
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
|
|
}
|