From 7efb9d2283bccdbf46d2b629fcfb6f286c81bf91 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Sun, 31 May 2026 20:36:10 +0800 Subject: [PATCH] feat: implement VerifyPathForSafeMode v1.4.1 --- file.go | 72 +++++++++++++++++++ js_export.go | 191 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 231 insertions(+), 32 deletions(-) diff --git a/file.go b/file.go index ebf543c..63f090e 100644 --- a/file.go +++ b/file.go @@ -2,6 +2,7 @@ package file import ( "bufio" + "context" "fmt" "io" "os" @@ -9,6 +10,9 @@ import ( "path/filepath" "strings" "time" + + "apigo.cc/go/cast" + "apigo.cc/go/jsmod" ) type FileInfo struct { @@ -268,3 +272,71 @@ func RunCommand(command string, args ...string) ([]string, error) { } 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 +} diff --git a/js_export.go b/js_export.go index e8cda83..1fab1e9 100644 --- a/js_export.go +++ b/js_export.go @@ -1,38 +1,165 @@ package file -import "apigo.cc/go/jsmod" +import ( + "context" + + "apigo.cc/go/jsmod" +) func init() { jsmod.Register("file", map[string]any{ - "exists": Exists, - "read": Read, - "readBytes": ReadBytes, - "readLines": ReadLines, - "readDir": ReadDir, - "getFileInfo": GetFileInfo, - "getAbsFilename": GetAbsFilename, - "write": Write, - "writeBytes": WriteBytes, - "remove": Remove, - "mkdir": Mkdir, - "copy": Copy, - "move": Move, - "replace": Replace, - "unmarshalFile": UnmarshalFile, - "marshalFile": MarshalFile, - "marshalFilePretty": MarshalFilePretty, - "patchFile": PatchFile, - "archive": Archive, - "extract": Extract, - "compress": Compress, - "decompress": Decompress, - "readFileFromMemory": ReadFileFromMemory, - "readDirFromMemory": ReadDirFromMemory, - }, - "exists", "read", "readBytes", "readLines", "readDir", "getFileInfo", "getAbsFilename", - "write", "writeBytes", "remove", "mkdir", "copy", "move", "replace", - "unmarshalFile", "marshalFile", "marshalFilePretty", "patchFile", - "archive", "extract", "compress", "decompress", - "readFileFromMemory", "readDirFromMemory", - ) + // 读操作 (映射到私有包装器) + "exists": func(ctx context.Context, path string) bool { + p, err := VerifyPathForSafeMode(ctx, path) + if err != nil { + return false + } + return Exists(p) + }, + "read": func(ctx context.Context, path string) (string, error) { + p, err := VerifyPathForSafeMode(ctx, path) + if err != nil { + return "", err + } + return Read(p) + }, + "readBytes": func(ctx context.Context, path string) ([]byte, error) { + p, err := VerifyPathForSafeMode(ctx, path) + if err != nil { + return nil, err + } + return ReadBytes(p) + }, + "readLines": func(ctx context.Context, path string) ([]string, error) { + p, err := VerifyPathForSafeMode(ctx, path) + if err != nil { + return nil, err + } + return ReadLines(p) + }, + "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, + }) }