fix(localio): reject special files before open
This commit is contained in:
1 parent
3d67d83110
commit
f413db06be
4 files changed
+110
No files matched your search
@@ -15,6 +15,7 @@ const defaultTextInputLimit = int64(8 << 20)
|
||||
|
||||
var (
|
||||
readTextInputAll = io.ReadAll
|
||||
statTextInputPath = os.Stat
|
||||
openTextInputFile = os.Open
|
||||
readTextInputRel = filepath.Rel
|
||||
)
|
||||
@@ -72,6 +73,16 @@ func ReadTextInput(spec string, stdin io.Reader, maxBytes int64) (string, error)
|
||||
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) {
|
||||
return "", fmt.Errorf("LOCAL_INPUT_UNSAFE: @file 解析后逃逸工作目录")
|
||||
}
|
||||
pathInfo, err := statTextInputPath(realPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("LOCAL_INPUT_READ_FAILED: %w", err)
|
||||
}
|
||||
if !pathInfo.Mode().IsRegular() {
|
||||
return "", fmt.Errorf("LOCAL_INPUT_INVALID: @file 必须是普通文件")
|
||||
}
|
||||
if pathInfo.Size() > maxBytes {
|
||||
return "", fmt.Errorf("LOCAL_INPUT_TOO_LARGE: 文件大小 %d 超过 %d 字节", pathInfo.Size(), maxBytes)
|
||||
}
|
||||
file, err := openTextInputFile(realPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("LOCAL_INPUT_READ_FAILED: %w", err)
|
||||
|
||||
@@ -72,4 +72,54 @@ func TestCrossPlatformCoverageReadTextInputUsesOpenedDescriptorAndBoundedReadE2E
|
||||
t.Fatal("file growth bypassed size limit")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("path becomes directory before descriptor check", func(t *testing.T) {
|
||||
path := filepath.Join(dir, "becomes-directory.txt")
|
||||
if err := os.WriteFile(path, []byte("safe"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testseam.Swap(t, &openTextInputFile, func(candidate string) (*os.File, error) {
|
||||
if removeErr := os.Remove(candidate); removeErr != nil {
|
||||
return nil, removeErr
|
||||
}
|
||||
if mkdirErr := os.Mkdir(candidate, 0o700); mkdirErr != nil {
|
||||
return nil, mkdirErr
|
||||
}
|
||||
return os.Open(candidate)
|
||||
})
|
||||
if _, err := ReadTextInput("@becomes-directory.txt", nil, 10); err == nil {
|
||||
t.Fatal("path type replacement bypassed descriptor validation")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file grows before descriptor check", func(t *testing.T) {
|
||||
path := filepath.Join(dir, "grows-before-stat.txt")
|
||||
if err := os.WriteFile(path, []byte("ok"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testseam.Swap(t, &openTextInputFile, func(candidate string) (*os.File, error) {
|
||||
file, openErr := os.Open(candidate)
|
||||
if openErr != nil {
|
||||
return nil, openErr
|
||||
}
|
||||
writer, writeOpenErr := os.OpenFile(candidate, os.O_APPEND|os.O_WRONLY, 0)
|
||||
if writeOpenErr != nil {
|
||||
_ = file.Close()
|
||||
return nil, writeOpenErr
|
||||
}
|
||||
if _, writeErr := writer.WriteString("-too-large"); writeErr != nil {
|
||||
_ = writer.Close()
|
||||
_ = file.Close()
|
||||
return nil, writeErr
|
||||
}
|
||||
if closeErr := writer.Close(); closeErr != nil {
|
||||
_ = file.Close()
|
||||
return nil, closeErr
|
||||
}
|
||||
return file, nil
|
||||
})
|
||||
if _, err := ReadTextInput("@grows-before-stat.txt", nil, 4); err == nil {
|
||||
t.Fatal("pre-open growth bypassed descriptor size validation")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//go:build unix
|
||||
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
package localio
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoverageReadTextInputRejectsUnconnectedFIFOWithoutBlockingE2E(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
old, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chdir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chdir(old) })
|
||||
|
||||
fifo := filepath.Join(dir, "input.fifo")
|
||||
if err := syscall.Mkfifo(fifo, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, readErr := ReadTextInput("@input.fifo", nil, 1024)
|
||||
result <- readErr
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-result:
|
||||
if err == nil || !strings.Contains(err.Error(), "LOCAL_INPUT_INVALID") {
|
||||
t.Fatalf("FIFO rejection error = %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("FIFO without a writer blocked text input validation")
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,9 @@ func TestCrossPlatformCoverageReadTextInputFailureBranchesE2E(t *testing.T) {
|
||||
"relative": func(t *testing.T) {
|
||||
testseam.Swap(t, &readTextInputRel, func(string, string) (string, error) { return "", errors.New("rel") })
|
||||
},
|
||||
"path stat": func(t *testing.T) {
|
||||
testseam.Swap(t, &statTextInputPath, func(string) (os.FileInfo, error) { return nil, errors.New("stat") })
|
||||
},
|
||||
"open": func(t *testing.T) {
|
||||
testseam.Swap(t, &openTextInputFile, func(string) (*os.File, error) { return nil, errors.New("open") })
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user