Bound external result file reads
This commit is contained in:
@@ -108,3 +108,14 @@ func ReadRegularFileUnderRoot(
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// ReadRegularFile reads a bounded regular file without following symbolic links
|
||||
// in its parent hierarchy. It is intended for externally produced results;
|
||||
// callers own the limit and semantic validation contract.
|
||||
func ReadRegularFile(path string, maxBytes int64) ([]byte, error) {
|
||||
clean := filepath.Clean(path)
|
||||
if clean == "." || filepath.Base(clean) == "." {
|
||||
return nil, fmt.Errorf("file path is required")
|
||||
}
|
||||
return ReadRegularFileUnderRoot(filepath.Dir(clean), filepath.Base(clean), maxBytes, nil, nil)
|
||||
}
|
||||
|
||||
65
internal/fileops/read_confined_test.go
Normal file
65
internal/fileops/read_confined_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package fileops
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadRegularFileEnforcesLimitAndRegularFileIdentity(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, "result.json")
|
||||
if err := os.WriteFile(path, []byte("1234"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
data, err := ReadRegularFile(path, 4)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadRegularFile(exact limit) error = %v", err)
|
||||
}
|
||||
if string(data) != "1234" {
|
||||
t.Fatalf("data = %q, want exact file content", data)
|
||||
}
|
||||
if _, err := ReadRegularFile(path, 3); err == nil || !strings.Contains(err.Error(), "3-byte limit") {
|
||||
t.Fatalf("ReadRegularFile(limit plus one) error = %v, want limit failure", err)
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Fatalf("Remove(file) error = %v", err)
|
||||
}
|
||||
if err := os.Mkdir(path, 0o700); err != nil {
|
||||
t.Fatalf("Mkdir(non-regular file) error = %v", err)
|
||||
}
|
||||
if _, err := ReadRegularFile(path, 4); err == nil || !strings.Contains(err.Error(), "not a regular file") {
|
||||
t.Fatalf("ReadRegularFile(directory) error = %v, want regular-file failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRegularFileRejectsSymlinkedPaths(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlink behavior differs on windows")
|
||||
}
|
||||
root := t.TempDir()
|
||||
outside := t.TempDir()
|
||||
outsideFile := filepath.Join(outside, "result.json")
|
||||
if err := os.WriteFile(outsideFile, []byte("outside"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(outside) error = %v", err)
|
||||
}
|
||||
leaf := filepath.Join(root, "result.json")
|
||||
if err := os.Symlink(outsideFile, leaf); err != nil {
|
||||
t.Fatalf("Symlink(leaf) error = %v", err)
|
||||
}
|
||||
if _, err := ReadRegularFile(leaf, 64); err == nil {
|
||||
t.Fatal("ReadRegularFile(leaf symlink) error = nil, want rejection")
|
||||
}
|
||||
if err := os.Remove(leaf); err != nil {
|
||||
t.Fatalf("Remove(leaf symlink) error = %v", err)
|
||||
}
|
||||
linkedParent := filepath.Join(root, "linked")
|
||||
if err := os.Symlink(outside, linkedParent); err != nil {
|
||||
t.Fatalf("Symlink(parent) error = %v", err)
|
||||
}
|
||||
if _, err := ReadRegularFile(filepath.Join(linkedParent, "result.json"), 64); err == nil {
|
||||
t.Fatal("ReadRegularFile(symlinked parent) error = nil, want rejection")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user