// Package safeio reads and writes local files without following symlinked path components. package safeio import ( "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.Stat(path) if err != nil || !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 := file.Write(contents); err != nil { _ = os.Remove(path) return ErrUnsafeFile } if err := file.Sync(); err != nil { _ = os.Remove(path) return ErrUnsafeFile } return 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 }