Harden API key file loading

This commit is contained in:
2026-08-10 18:24:29 +00:00
parent 363313d99c
commit 99b2e1cd81
13 changed files with 370 additions and 32 deletions

View File

@@ -0,0 +1,110 @@
package fileops
import (
"fmt"
"io"
"os"
"path/filepath"
)
// ValidateConfinedDirectory opens path without following symbolic links and
// applies validate to the opened directory's metadata.
func ValidateConfinedDirectory(path string, validate func(os.FileInfo) error) error {
root, err := openConfinedDirectory(path)
if err != nil {
return err
}
defer func() { _ = root.Close() }()
info, err := root.Stat(".")
if err != nil {
return fmt.Errorf("inspect root directory: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("root path is not a directory")
}
if validate != nil {
return validate(info)
}
return nil
}
// ReadRegularFileUnderRoot reads a bounded regular file after opening root and
// every relative-path ancestor without following symbolic links. validateRoot
// and validateFile may enforce caller-owned access policy while their handles
// are still verified.
func ReadRegularFileUnderRoot(
rootPath, relativePath string,
maxBytes int64,
validateRoot, validateFile func(os.FileInfo) error,
) ([]byte, error) {
if maxBytes < 0 {
return nil, fmt.Errorf("maximum byte count must not be negative")
}
if filepath.IsAbs(relativePath) {
return nil, fmt.Errorf("relative file path must not be absolute")
}
parts, err := relativePathParts(relativePath)
if err != nil {
return nil, fmt.Errorf("invalid relative file path: %w", err)
}
root, err := openConfinedDirectory(rootPath)
if err != nil {
return nil, err
}
defer func() { _ = root.Close() }()
rootInfo, err := root.Stat(".")
if err != nil {
return nil, fmt.Errorf("inspect root directory: %w", err)
}
if validateRoot != nil {
if err := validateRoot(rootInfo); err != nil {
return nil, err
}
}
for _, part := range parts[:len(parts)-1] {
child, err := openConfinedChild(root, part, false, 0)
if err != nil {
return nil, err
}
_ = root.Close()
root = child
}
name := parts[len(parts)-1]
inspected, err := root.Lstat(name)
if err != nil {
return nil, fmt.Errorf("inspect file %q: %w", name, err)
}
if inspected.Mode()&os.ModeSymlink != 0 || !inspected.Mode().IsRegular() {
return nil, fmt.Errorf("file %q is not a regular file", name)
}
file, err := root.Open(name)
if err != nil {
return nil, fmt.Errorf("open file %q: %w", name, err)
}
defer func() { _ = file.Close() }()
opened, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("inspect opened file %q: %w", name, err)
}
current, err := root.Lstat(name)
if err != nil || current.Mode()&os.ModeSymlink != 0 || !current.Mode().IsRegular() || !os.SameFile(opened, current) {
if err != nil {
return nil, fmt.Errorf("reinspect file %q: %w", name, err)
}
return nil, fmt.Errorf("file %q changed while being opened", name)
}
if validateFile != nil {
if err := validateFile(opened); err != nil {
return nil, err
}
}
content, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
if err != nil {
return nil, fmt.Errorf("read file %q: %w", name, err)
}
if int64(len(content)) > maxBytes {
return nil, fmt.Errorf("file %q exceeds %d-byte limit", name, maxBytes)
}
return content, nil
}