Harden API key file loading
This commit is contained in:
110
internal/fileops/read_confined.go
Normal file
110
internal/fileops/read_confined.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user