127 lines
3.6 KiB
Go
127 lines
3.6 KiB
Go
// Package safeio reads and writes local files without following symlinked path components.
|
|
package safeio
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
func readBoundedRegularFile(path string, 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() || !hasSingleLink(info) {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
contents, err := io.ReadAll(io.LimitReader(file, maximum+1))
|
|
if err != nil || int64(len(contents)) > maximum {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
after, err := file.Stat()
|
|
if err != nil || !after.Mode().IsRegular() || !hasSingleLink(after) || !os.SameFile(info, after) {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
current, err := os.Lstat(path)
|
|
if err != nil || !current.Mode().IsRegular() || current.Mode()&os.ModeSymlink != 0 || !hasSingleLink(current) || !os.SameFile(info, current) {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
return contents, nil
|
|
}
|
|
|
|
func ReadCanonicalUTF8(path string, maximum int64) (string, error) {
|
|
contents, err := ReadCanonicalRegular(path, maximum)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !utf8.Valid(contents) {
|
|
return "", ErrUnsafeFile
|
|
}
|
|
return string(contents), nil
|
|
}
|
|
|
|
func WriteCanonicalNewFile(path string, contents []byte, mode os.FileMode) error {
|
|
if err := ValidateCanonicalPath(path); err != nil {
|
|
return err
|
|
}
|
|
parent := filepath.Dir(path)
|
|
if err := requireCanonicalDirectory(parent); err != nil {
|
|
return err
|
|
}
|
|
if info, err := os.Lstat(path); err == nil {
|
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Mode()&os.ModeType != 0 {
|
|
return ErrUnsafeFile
|
|
}
|
|
return ErrUnsafeFile
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return ErrUnsafeFile
|
|
}
|
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
|
|
if err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
defer file.Close()
|
|
if err := ProtectPrivateRegular(path); err != nil {
|
|
_ = os.Remove(path)
|
|
return ErrUnsafeFile
|
|
}
|
|
if _, err := file.Write(contents); err != nil {
|
|
_ = os.Remove(path)
|
|
return ErrUnsafeFile
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
_ = os.Remove(path)
|
|
return ErrUnsafeFile
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReplaceCanonicalRegular durably replaces one existing private regular file without following
|
|
// symlinked path components. Platform implementations keep the temporary file in the target
|
|
// directory and use the platform's atomic replace primitive.
|
|
func ReplaceCanonicalRegular(path string, contents []byte, mode os.FileMode) error {
|
|
if err := ValidateCanonicalPath(path); err != nil || mode.Perm() != 0o600 || mode&os.ModeType != 0 {
|
|
return ErrUnsafeFile
|
|
}
|
|
return replaceCanonicalRegular(path, contents)
|
|
}
|
|
|
|
func randomTemporaryName() (string, error) {
|
|
bytes := make([]byte, 16)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
return ".tht-auth-" + hex.EncodeToString(bytes) + ".tmp", nil
|
|
}
|
|
|
|
func requireCanonicalDirectory(path string) error {
|
|
if err := ValidateCanonicalPath(path); err != nil {
|
|
return err
|
|
}
|
|
resolved, err := filepath.EvalSymlinks(path)
|
|
if err != nil || resolved != path {
|
|
return ErrUnsafeFile
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil || !info.IsDir() {
|
|
return ErrUnsafeFile
|
|
}
|
|
return nil
|
|
}
|