// 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 }