feat: implement VerifyPathForSafeMode v1.4.1

This commit is contained in:
AI Engineer 2026-05-31 20:36:10 +08:00
parent 18e85b1ff1
commit daf1fa2289
2 changed files with 231 additions and 32 deletions

72
file.go
View File

@ -2,6 +2,7 @@ package file
import ( import (
"bufio" "bufio"
"context"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -9,6 +10,9 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"apigo.cc/go/cast"
"apigo.cc/go/jsmod"
) )
type FileInfo struct { type FileInfo struct {
@ -268,3 +272,71 @@ func RunCommand(command string, args ...string) ([]string, error) {
} }
return lines, nil return lines, nil
} }
// RealPath 返回客观的物理绝对路径,处理软连接和相对路径
func RealPath(path string) string {
abs, err := filepath.Abs(path)
if err != nil {
return path
}
// 尝试解析软连接
real, err := filepath.EvalSymlinks(abs)
if err == nil {
return real
}
// 如果文件不存在EvalSymlinks 会失败,此时返回绝对路径即可(后续写入操作会处理)
return abs
}
// VerifyPathForSafeMode 在安全模式下校验路径是否在 AllowedDirs 白名单内
func VerifyPathForSafeMode(ctx context.Context, path string) (string, error) {
// 如果不是安全模式,直接返回 RealPath
if ctx == nil || !jsmod.IsSafeMode(ctx) {
return RealPath(path), nil
}
// 1. 获取白名单 (约定从 ctx 获取 "AllowedDirs")
var allowedDirs []string
cast.Convert(&allowedDirs, ctx.Value("AllowedDirs"))
if len(allowedDirs) == 0 {
return "", fmt.Errorf("file: access denied, AllowedDirs not found in context")
}
// 2. 获取物理路径
real := RealPath(path)
// 3. 校验逻辑:判断当前物理路径是否以任何一个白名单目录为前缀
isAllowed := false
for _, dir := range allowedDirs {
if strings.HasPrefix(real, dir) {
isAllowed = true
break
}
}
// 4. 写操作保护:如果路径不存在,追溯其存在的父目录权限
if !isAllowed {
temp := real
for temp != "" && temp != string(os.PathSeparator) {
temp = filepath.Dir(temp)
if _, err := os.Stat(temp); err == nil {
// 找到了第一个存在的祖先目录,检查它是否受控
for _, dir := range allowedDirs {
if strings.HasPrefix(temp, dir) {
isAllowed = true
break
}
}
break
}
}
}
if !isAllowed {
return "", fmt.Errorf("file: permission denied for path %s", path)
}
return real, nil
}

View File

@ -1,38 +1,165 @@
package file package file
import "apigo.cc/go/jsmod" import (
"context"
"apigo.cc/go/jsmod"
)
func init() { func init() {
jsmod.Register("file", map[string]any{ jsmod.Register("file", map[string]any{
"exists": Exists, // 读操作 (映射到私有包装器)
"read": Read, "exists": func(ctx context.Context, path string) bool {
"readBytes": ReadBytes, p, err := VerifyPathForSafeMode(ctx, path)
"readLines": ReadLines, if err != nil {
"readDir": ReadDir, return false
"getFileInfo": GetFileInfo, }
"getAbsFilename": GetAbsFilename, return Exists(p)
"write": Write, },
"writeBytes": WriteBytes, "read": func(ctx context.Context, path string) (string, error) {
"remove": Remove, p, err := VerifyPathForSafeMode(ctx, path)
"mkdir": Mkdir, if err != nil {
"copy": Copy, return "", err
"move": Move, }
"replace": Replace, return Read(p)
"unmarshalFile": UnmarshalFile, },
"marshalFile": MarshalFile, "readBytes": func(ctx context.Context, path string) ([]byte, error) {
"marshalFilePretty": MarshalFilePretty, p, err := VerifyPathForSafeMode(ctx, path)
"patchFile": PatchFile, if err != nil {
"archive": Archive, return nil, err
"extract": Extract, }
"compress": Compress, return ReadBytes(p)
"decompress": Decompress, },
"readFileFromMemory": ReadFileFromMemory, "readLines": func(ctx context.Context, path string) ([]string, error) {
"readDirFromMemory": ReadDirFromMemory, p, err := VerifyPathForSafeMode(ctx, path)
}, if err != nil {
"exists", "read", "readBytes", "readLines", "readDir", "getFileInfo", "getAbsFilename", return nil, err
"write", "writeBytes", "remove", "mkdir", "copy", "move", "replace", }
"unmarshalFile", "marshalFile", "marshalFilePretty", "patchFile", return ReadLines(p)
"archive", "extract", "compress", "decompress", },
"readFileFromMemory", "readDirFromMemory", "readDir": func(ctx context.Context, path string) ([]FileInfo, error) {
) p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return nil, err
}
return ReadDir(p)
},
"getFileInfo": func(ctx context.Context, path string) *FileInfo {
p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return nil
}
return GetFileInfo(p)
},
// 写操作
"write": func(ctx context.Context, path string, content string) error {
p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
}
return Write(p, content)
},
"writeBytes": func(ctx context.Context, path string, content []byte) error {
p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
}
return WriteBytes(p, content)
},
"remove": func(ctx context.Context, path string) error {
p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
}
return Remove(p)
},
"mkdir": func(ctx context.Context, path string) error {
p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
}
return Mkdir(p)
},
"copy": func(ctx context.Context, from, to string) error {
pFrom, err := VerifyPathForSafeMode(ctx, from)
if err != nil {
return err
}
pTo, err := VerifyPathForSafeMode(ctx, to)
if err != nil {
return err
}
return Copy(pFrom, pTo)
},
"move": func(ctx context.Context, from, to string) error {
pFrom, err := VerifyPathForSafeMode(ctx, from)
if err != nil {
return err
}
pTo, err := VerifyPathForSafeMode(ctx, to)
if err != nil {
return err
}
return Move(pFrom, pTo)
},
"replace": func(ctx context.Context, path, old, new string) error {
p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
}
return Replace(p, old, new)
},
// 序列化
"unmarshalFile": func(ctx context.Context, path string, to any) error {
p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
}
return UnmarshalFile(p, to)
},
"marshalFile": func(ctx context.Context, path string, data any) error {
p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
}
return MarshalFile(p, data)
},
"marshalFilePretty": func(ctx context.Context, path string, data any) error {
p, err := VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
}
return MarshalFilePretty(p, data)
},
// 归档
"archive": func(ctx context.Context, src, dest string) error {
pSrc, err := VerifyPathForSafeMode(ctx, src)
if err != nil {
return err
}
pDest, err := VerifyPathForSafeMode(ctx, dest)
if err != nil {
return err
}
return Archive(pSrc, pDest)
},
"extract": func(ctx context.Context, src, dest string, strip bool) error {
pSrc, err := VerifyPathForSafeMode(ctx, src)
if err != nil {
return err
}
pDest, err := VerifyPathForSafeMode(ctx, dest)
if err != nil {
return err
}
return Extract(pSrc, pDest, strip)
},
// 压缩工具 (无路径,不校验)
"compress": Compress,
"decompress": Decompress,
})
} }