diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-19 21:51:44 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-19 21:51:44 +0200 |
| commit | 2ab8a24c188a2ba39424eb7925bc7ff3fb767bfb (patch) | |
| tree | 0867a5d189d61a6e7f6ce4accea9868014a0fe7d /internal/io/fs/validatedreadtarget.go | |
| parent | 91296d85e8a6f1aca5beaeeecf648683c83c75bc (diff) | |
task 261: harden server reads with OpenRoot
Diffstat (limited to 'internal/io/fs/validatedreadtarget.go')
| -rw-r--r-- | internal/io/fs/validatedreadtarget.go | 90 |
1 files changed, 90 insertions, 0 deletions
diff --git a/internal/io/fs/validatedreadtarget.go b/internal/io/fs/validatedreadtarget.go new file mode 100644 index 0000000..83b1404 --- /dev/null +++ b/internal/io/fs/validatedreadtarget.go @@ -0,0 +1,90 @@ +package fs + +import ( + "fmt" + "os" + "path/filepath" +) + +// ValidatedReadTarget stores a resolved regular file path for rooted re-opens. +type ValidatedReadTarget struct { + resolvedPath string + rootDir string + rootName string +} + +// NewValidatedReadTarget returns a rooted target for a resolved regular file. +func NewValidatedReadTarget(resolvedPath string) (ValidatedReadTarget, error) { + cleanedPath := filepath.Clean(resolvedPath) + if !filepath.IsAbs(cleanedPath) { + return ValidatedReadTarget{}, fmt.Errorf("validated read target requires absolute path: %s", cleanedPath) + } + + info, err := os.Lstat(cleanedPath) + if err != nil { + return ValidatedReadTarget{}, fmt.Errorf("lstat validated read target %s: %w", cleanedPath, err) + } + if !info.Mode().IsRegular() { + return ValidatedReadTarget{}, fmt.Errorf("validated read target must be a regular file: %s", cleanedPath) + } + + return ValidatedReadTarget{ + resolvedPath: cleanedPath, + rootDir: filepath.Dir(cleanedPath), + rootName: filepath.Base(cleanedPath), + }, nil +} + +// Open re-opens the validated file beneath its resolved parent directory. +func (t ValidatedReadTarget) Open() (*os.File, error) { + root, err := os.OpenRoot(t.rootDir) + if err != nil { + return nil, fmt.Errorf("open root for %s: %w", t.resolvedPath, err) + } + defer root.Close() + + if err := t.validateEntry(root); err != nil { + return nil, err + } + + fd, err := root.Open(t.rootName) + if err != nil { + return nil, fmt.Errorf("open rooted file %s: %w", t.resolvedPath, err) + } + + if err := validateOpenedFile(fd, t.resolvedPath); err != nil { + fd.Close() + return nil, err + } + if err := t.validateEntry(root); err != nil { + fd.Close() + return nil, err + } + + return fd, nil +} + +func (t ValidatedReadTarget) validateEntry(root *os.Root) error { + info, err := root.Lstat(t.rootName) + if err != nil { + return fmt.Errorf("lstat rooted file %s: %w", t.resolvedPath, err) + } + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("rooted file changed to symlink: %s", t.resolvedPath) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("rooted file changed to non-regular file: %s", t.resolvedPath) + } + return nil +} + +func validateOpenedFile(fd *os.File, resolvedPath string) error { + info, err := fd.Stat() + if err != nil { + return fmt.Errorf("stat opened file %s: %w", resolvedPath, err) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("opened file is not regular: %s", resolvedPath) + } + return nil +} |
