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