71 lines
2.4 KiB
Go
71 lines
2.4 KiB
Go
// Package safeio reads installation files without following symlinked path components.
|
|
package safeio
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var ErrUnsafeFile = errors.New("unsafe file")
|
|
|
|
// ErrIndeterminateFile means publication cleanup could not establish whether a
|
|
// private candidate is still named. Callers must reconcile the destination and
|
|
// private stages before retrying; it is never a blind-retry-safe failure.
|
|
var ErrIndeterminateFile = errors.New("indeterminate file state")
|
|
|
|
// beforeBoundedRead is an internal test seam used to deterministically suspend
|
|
// a read between opening the file and resolving its final pathname.
|
|
var beforeBoundedRead func()
|
|
|
|
// 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
|
|
}
|
|
if err := validatePlatformPathSyntax(path); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readBoundedRegularFile(file *os.File, maximum int64) ([]byte, error) {
|
|
if maximum < 0 || maximum == int64(^uint64(0)>>1) {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
info, err := file.Stat()
|
|
if err != nil || !info.Mode().IsRegular() {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
if beforeBoundedRead != nil {
|
|
beforeBoundedRead()
|
|
}
|
|
contents, err := io.ReadAll(io.LimitReader(file, maximum+1))
|
|
if err != nil || int64(len(contents)) > maximum {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
return contents, nil
|
|
}
|
|
|
|
// ReadCanonicalUTF8 reads a canonical regular file with a strict byte bound and UTF-8 validation.
|
|
func ReadCanonicalUTF8(path string, maximum int64) ([]byte, error) {
|
|
contents, err := ReadCanonicalRegular(path, maximum)
|
|
if err != nil || !utf8.Valid(contents) {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
return contents, nil
|
|
}
|
|
|
|
// WriteCanonicalExclusive creates a canonical regular file without following links or replacing
|
|
// an existing leaf. The file is private to the caller and is never opened in truncate mode.
|
|
func WriteCanonicalExclusive(path string, contents []byte, mode fs.FileMode) error {
|
|
return writeCanonicalExclusive(path, contents, mode)
|
|
}
|
|
|
|
// ValidateCanonicalOutputPath verifies every parent directory without creating the leaf.
|
|
func ValidateCanonicalOutputPath(path string) error { return validateCanonicalOutputPath(path) }
|