From c75139450a6d62cb2c94312ce772020f47356b74 Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 16 Jul 2026 10:26:43 +0700 Subject: [PATCH] fix(extensions): bound file reads --- go_backend/extension_runtime_file.go | 57 ++++++++++++++----- .../extension_runtime_file_limits_test.go | 56 ++++++++++++++++++ 2 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 go_backend/extension_runtime_file_limits_test.go diff --git a/go_backend/extension_runtime_file.go b/go_backend/extension_runtime_file.go index c2143129..da0d9ad8 100644 --- a/go_backend/extension_runtime_file.go +++ b/go_backend/extension_runtime_file.go @@ -719,16 +719,48 @@ func (r *extensionRuntime) fileRead(call goja.FunctionCall) goja.Value { return r.jsError("%s", err.Error()) } - data, err := os.ReadFile(fullPath) + file, err := os.Open(fullPath) if err != nil { return r.jsError("%s", err.Error()) } + defer file.Close() + + data, err := io.ReadAll(io.LimitReader(file, maxExtensionFileReadBytes+1)) + if err != nil { + return r.jsError("%s", err.Error()) + } + if int64(len(data)) > maxExtensionFileReadBytes { + return r.jsError(extensionFileReadLimitError) + } return r.jsSuccess(map[string]any{ "data": string(data), }) } +const ( + maxExtensionFileReadBytes = int64(16 << 20) + extensionFileReadLimitError = "file read exceeds 16 MiB limit; use file.readBytes with offset and length to read it in chunks" +) + +func extensionFileReadLength(size, offset, requested int64) (int64, error) { + remaining := size - offset + if remaining < 0 { + remaining = 0 + } + + if requested < 0 { + if remaining > maxExtensionFileReadBytes { + return 0, fmt.Errorf("%s", extensionFileReadLimitError) + } + return remaining, nil + } + if requested > maxExtensionFileReadBytes { + return 0, fmt.Errorf("%s", extensionFileReadLimitError) + } + return min(requested, remaining), nil +} + func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.jsError("path is required") @@ -766,22 +798,17 @@ func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value { return r.jsError("failed to seek file: %v", err) } - var data []byte - switch { - case length == 0: - data = []byte{} - case length > 0: - buf := make([]byte, int(length)) - n, readErr := file.Read(buf) - if readErr != nil && readErr != io.EOF { + readLength, err := extensionFileReadLength(size, offset, length) + if err != nil { + return r.jsError("%s", err.Error()) + } + data := make([]byte, int(readLength)) + if readLength > 0 { + n, readErr := io.ReadFull(file, data) + if readErr != nil && readErr != io.EOF && readErr != io.ErrUnexpectedEOF { return r.jsError("failed to read file: %v", readErr) } - data = buf[:n] - default: - data, err = io.ReadAll(file) - if err != nil { - return r.jsError("failed to read file: %v", err) - } + data = data[:n] } if strings.EqualFold(strings.TrimSpace(encoding), "bytes") || diff --git a/go_backend/extension_runtime_file_limits_test.go b/go_backend/extension_runtime_file_limits_test.go new file mode 100644 index 00000000..d67547fe --- /dev/null +++ b/go_backend/extension_runtime_file_limits_test.go @@ -0,0 +1,56 @@ +package gobackend + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/dop251/goja" +) + +func TestExtensionFileReadLengthRequiresChunking(t *testing.T) { + if _, err := extensionFileReadLength(maxExtensionFileReadBytes+1, 0, -1); err == nil { + t.Fatal("unbounded large read was accepted") + } + if _, err := extensionFileReadLength(maxExtensionFileReadBytes+1, 0, maxExtensionFileReadBytes+1); err == nil { + t.Fatal("oversized explicit read was accepted") + } + if got, err := extensionFileReadLength(maxExtensionFileReadBytes+1, maxExtensionFileReadBytes, -1); err != nil || got != 1 { + t.Fatalf("tail read length = %d, %v", got, err) + } + if got, err := extensionFileReadLength(5, 2, 10); err != nil || got != 3 { + t.Fatalf("clamped read length = %d, %v", got, err) + } +} + +func TestExtensionFileAPIsRejectUnboundedLargeReads(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "large.bin") + file, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + if err := file.Truncate(maxExtensionFileReadBytes + 1); err != nil { + file.Close() + t.Fatal(err) + } + file.Close() + + vm := goja.New() + runtime := &extensionRuntime{ + vm: vm, + dataDir: dir, + manifest: &ExtensionManifest{Permissions: ExtensionPermissions{File: true}}, + } + + readResult := runtime.fileRead(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("large.bin")}}).Export().(map[string]any) + if readResult["success"] != false || !strings.Contains(readResult["error"].(string), "chunks") { + t.Fatalf("fileRead result = %#v", readResult) + } + + readBytesResult := runtime.fileReadBytes(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("large.bin")}}).Export().(map[string]any) + if readBytesResult["success"] != false || !strings.Contains(readBytesResult["error"].(string), "chunks") { + t.Fatalf("fileReadBytes result = %#v", readBytesResult) + } +}