package service import ( "fmt" "net/url" "strings" "with_you/internal/model" "with_you/internal/pkg/netutil" ) // ValidateChatMessageImageSegments 校验聊天消息中的图片段:禁止 data:/base64 内联,仅允许引用本站 S3 公开前缀下的 URL。 func (s *UploadService) ValidateChatMessageImageSegments(segments model.MessageSegments) error { if s == nil || s.s3Client == nil { return nil } prefix := strings.TrimSpace(s.s3Client.TrustedPublicURLPrefix()) if prefix == "" { return nil } for _, seg := range segments { if seg.Type != string(model.ContentTypeImage) && seg.Type != "image" { continue } if seg.Data == nil { return fmt.Errorf("图片消息数据无效") } urlStr, _ := seg.Data["url"].(string) urlStr = strings.TrimSpace(urlStr) if urlStr == "" { return fmt.Errorf("图片消息缺少 url") } low := strings.ToLower(urlStr) if strings.HasPrefix(low, "data:") || strings.HasPrefix(low, "base64:") || strings.HasPrefix(low, "base64://") { return fmt.Errorf("禁止在消息中内联 base64 或 data URL,请先通过上传接口获取图片地址") } if strings.ContainsAny(urlStr, "\n\r\x00") { return fmt.Errorf("非法图片地址") } u, err := url.Parse(urlStr) if err != nil || (u.Scheme != "http" && u.Scheme != "https") { return fmt.Errorf("图片地址必须为 http(s) 链接") } if u.Host == "" { return fmt.Errorf("图片地址无效") } if strings.HasPrefix(urlStr, prefix) { // 本站 S3 资源,跳过 SSRF 检查(内网 S3 可能解析为私有 IP) } else { if err := netutil.ValidateURL(urlStr); err != nil { return fmt.Errorf("图片地址不安全: %w", err) } if err := netutil.CheckResolvedHost(u.Hostname()); err != nil { return fmt.Errorf("图片地址不安全: %w", err) } return fmt.Errorf("图片必须使用本站上传接口生成的资源地址") } if thumb, ok := seg.Data["thumbnail_url"].(string); ok { thumb = strings.TrimSpace(thumb) if thumb != "" { if strings.HasPrefix(strings.ToLower(thumb), "data:") { return fmt.Errorf("禁止在图片消息中使用 data URL 缩略图") } if !strings.HasPrefix(thumb, prefix) { return fmt.Errorf("缩略图地址不合法") } } } } return nil }