mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-05 22:06:41 +02:00
Add files via upload
This commit is contained in:
@@ -15,8 +15,7 @@ type VisionConfig struct {
|
||||
JPEGQuality int `yaml:"jpeg_quality,omitempty" json:"jpeg_quality,omitempty"`
|
||||
MaxPayloadBytes int64 `yaml:"max_payload_bytes,omitempty" json:"max_payload_bytes,omitempty"`
|
||||
SkipPreprocessBelowBytes int64 `yaml:"skip_preprocess_below_bytes,omitempty" json:"skip_preprocess_below_bytes,omitempty"` // 0=始终压缩;默认 2MB 且长边已<=max_dimension 时原图直传
|
||||
Detail string `yaml:"detail,omitempty" json:"detail,omitempty"` // low | high | auto
|
||||
AllowedRoots []string `yaml:"allowed_roots,omitempty" json:"allowed_roots,omitempty"`
|
||||
Detail string `yaml:"detail,omitempty" json:"detail,omitempty"` // low | high | auto
|
||||
}
|
||||
|
||||
func (v VisionConfig) TimeoutSecondsEffective() int {
|
||||
|
||||
@@ -1548,9 +1548,6 @@ func updateVisionConfig(doc *yaml.Node, cfg config.VisionConfig) {
|
||||
if strings.TrimSpace(cfg.Detail) != "" {
|
||||
setStringInMap(visionNode, "detail", cfg.Detail)
|
||||
}
|
||||
if len(cfg.AllowedRoots) > 0 {
|
||||
setStringSliceInMap(visionNode, "allowed_roots", cfg.AllowedRoots)
|
||||
}
|
||||
}
|
||||
|
||||
func updateOpenAIConfig(doc *yaml.Node, cfg config.OpenAIConfig) {
|
||||
|
||||
@@ -809,8 +809,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
||||
"jpeg_quality": map[string]interface{}{"type": "integer", "description": "JPEG 质量 60-100"},
|
||||
"max_payload_bytes": map[string]interface{}{"type": "integer", "description": "送 API 体积上限(字节)"},
|
||||
"skip_preprocess_below_bytes": map[string]interface{}{"type": "integer", "description": "低于该字节且尺寸合规时可原图直传;0=始终压缩"},
|
||||
"detail": map[string]interface{}{"type": "string", "enum": []string{"low", "high", "auto"}, "description": "OpenAI 兼容 image detail"},
|
||||
"allowed_roots": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "额外允许读取的绝对路径根"},
|
||||
"detail": map[string]interface{}{"type": "string", "enum": []string{"low", "high", "auto"}, "description": "OpenAI 兼容 image detail"},
|
||||
},
|
||||
},
|
||||
"AnalyzeImageToolCall": map[string]interface{}{
|
||||
@@ -819,7 +818,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "图片路径(cwd、chat_uploads、result_storage_dir 或 allowed_roots 下)",
|
||||
"description": "图片绝对路径或相对于进程工作目录的路径",
|
||||
},
|
||||
"question": map[string]interface{}{
|
||||
"type": "string",
|
||||
|
||||
+9
-79
@@ -7,35 +7,26 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const chatUploadsDirName = "chat_uploads"
|
||||
|
||||
var allowedImageExt = map[string]struct{}{
|
||||
".png": {}, ".jpg": {}, ".jpeg": {}, ".webp": {}, ".gif": {},
|
||||
".bmp": {}, ".tif": {}, ".tiff": {},
|
||||
}
|
||||
|
||||
// PathOptions 图片路径白名单根目录。
|
||||
type PathOptions struct {
|
||||
CWD string
|
||||
ResultStorageDir string // 相对 CWD,如 tmp
|
||||
ExtraRoots []string // vision.allowed_roots 绝对路径
|
||||
}
|
||||
|
||||
// ResolveImagePath 解析并校验可读图片路径(防穿越、symlink 逃逸)。
|
||||
func ResolveImagePath(path string, opt PathOptions) (string, error) {
|
||||
// ResolveImagePath 解析并校验可读图片路径(支持任意目录;仍校验扩展名与常规文件)。
|
||||
func ResolveImagePath(path string, cwd string) (string, error) {
|
||||
p := strings.TrimSpace(path)
|
||||
if p == "" {
|
||||
return "", fmt.Errorf("path is empty")
|
||||
}
|
||||
cwd := strings.TrimSpace(opt.CWD)
|
||||
if cwd == "" {
|
||||
cwdTrim := strings.TrimSpace(cwd)
|
||||
if cwdTrim == "" {
|
||||
var err error
|
||||
cwd, err = os.Getwd()
|
||||
cwdTrim, err = os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getwd: %w", err)
|
||||
}
|
||||
}
|
||||
cwdAbs, err := filepath.Abs(filepath.Clean(cwd))
|
||||
cwdAbs, err := filepath.Abs(filepath.Clean(cwdTrim))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -46,22 +37,16 @@ func ResolveImagePath(path string, opt PathOptions) (string, error) {
|
||||
} else {
|
||||
candidate = filepath.Clean(filepath.Join(cwdAbs, p))
|
||||
}
|
||||
candidate = normalizeAbsPath(candidate)
|
||||
if candidate == "" {
|
||||
resolved := normalizeAbsPath(candidate)
|
||||
if resolved == "" {
|
||||
return "", fmt.Errorf("invalid path")
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(candidate))
|
||||
ext := strings.ToLower(filepath.Ext(resolved))
|
||||
if _, ok := allowedImageExt[ext]; !ok {
|
||||
return "", fmt.Errorf("unsupported image extension %q", ext)
|
||||
}
|
||||
|
||||
roots := buildAllowedRoots(cwdAbs, opt)
|
||||
resolved, err := evalUnderAllowedRoots(candidate, roots)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
st, err := os.Stat(resolved)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("stat: %w", err)
|
||||
@@ -85,58 +70,3 @@ func normalizeAbsPath(p string) string {
|
||||
}
|
||||
return abs
|
||||
}
|
||||
|
||||
func buildAllowedRoots(cwdAbs string, opt PathOptions) []string {
|
||||
seen := make(map[string]struct{})
|
||||
var roots []string
|
||||
add := func(r string) {
|
||||
r = strings.TrimSpace(r)
|
||||
if r == "" {
|
||||
return
|
||||
}
|
||||
abs := normalizeAbsPath(r)
|
||||
if abs == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[abs]; ok {
|
||||
return
|
||||
}
|
||||
seen[abs] = struct{}{}
|
||||
roots = append(roots, abs)
|
||||
}
|
||||
add(cwdAbs)
|
||||
add(filepath.Join(cwdAbs, chatUploadsDirName))
|
||||
rs := strings.TrimSpace(opt.ResultStorageDir)
|
||||
if rs == "" {
|
||||
rs = "tmp"
|
||||
}
|
||||
if filepath.IsAbs(rs) {
|
||||
add(rs)
|
||||
} else {
|
||||
add(filepath.Join(cwdAbs, rs))
|
||||
}
|
||||
for _, r := range opt.ExtraRoots {
|
||||
add(r)
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
func evalUnderAllowedRoots(candidate string, roots []string) (string, error) {
|
||||
check := normalizeAbsPath(candidate)
|
||||
for _, root := range roots {
|
||||
if isUnderRoot(check, root) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("path %q is outside allowed directories", candidate)
|
||||
}
|
||||
|
||||
func isUnderRoot(path, root string) bool {
|
||||
path = filepath.Clean(path)
|
||||
root = filepath.Clean(root)
|
||||
if path == root {
|
||||
return true
|
||||
}
|
||||
sep := string(filepath.Separator)
|
||||
return strings.HasPrefix(path, root+sep)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ func TestResolveImagePath_underCWD(t *testing.T) {
|
||||
if err := os.WriteFile(img, []byte{0x89, 0x50, 0x4e, 0x47}, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := ResolveImagePath(img, PathOptions{CWD: dir, ResultStorageDir: "tmp"})
|
||||
got, err := ResolveImagePath(img, dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -22,11 +22,20 @@ func TestResolveImagePath_underCWD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveImagePath_rejectsTraversal(t *testing.T) {
|
||||
func TestResolveImagePath_absoluteOutsideCWD(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, err := ResolveImagePath("../../../etc/passwd", PathOptions{CWD: dir})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for path outside roots")
|
||||
cwd := t.TempDir()
|
||||
img := filepath.Join(dir, "remote.png")
|
||||
if err := os.WriteFile(img, []byte{0x89, 0x50, 0x4e, 0x47}, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := ResolveImagePath(img, cwd)
|
||||
if err != nil {
|
||||
t.Fatalf("expected absolute path outside cwd to be allowed: %v", err)
|
||||
}
|
||||
want := normalizeAbsPath(img)
|
||||
if got != want {
|
||||
t.Fatalf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +45,7 @@ func TestResolveImagePath_rejectsNonImageExt(t *testing.T) {
|
||||
if err := os.WriteFile(f, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := ResolveImagePath(f, PathOptions{CWD: dir})
|
||||
_, err := ResolveImagePath(f, dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-image extension")
|
||||
}
|
||||
|
||||
@@ -33,11 +33,6 @@ func RegisterAnalyzeImageTool(mcpServer *mcp.Server, cfg *config.Config, logger
|
||||
return
|
||||
}
|
||||
|
||||
pathOpt := PathOptions{
|
||||
CWD: cwd,
|
||||
ResultStorageDir: cfg.Agent.ResultStorageDir,
|
||||
ExtraRoots: cfg.Vision.AllowedRoots,
|
||||
}
|
||||
preOpt := PreprocessOptions{
|
||||
MaxImageBytes: cfg.Vision.MaxImageBytesEffective(),
|
||||
MaxDimension: cfg.Vision.MaxDimensionEffective(),
|
||||
@@ -73,7 +68,7 @@ func RegisterAnalyzeImageTool(mcpServer *mcp.Server, cfg *config.Config, logger
|
||||
path, _ := args["path"].(string)
|
||||
question, _ := args["question"].(string)
|
||||
|
||||
abs, err := ResolveImagePath(path, pathOpt)
|
||||
abs, err := ResolveImagePath(path, cwd)
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("路径校验失败: %v", err), true), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user