Allow image segments with URLs matching the local S3 prefix to bypass SSRF validation, as internal S3 endpoints may resolve to private IP addresses that would otherwise trigger security errors.
75 lines
2.3 KiB
Go
75 lines
2.3 KiB
Go
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
|
||
}
|