- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion. - Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects. - Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management. - Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering. - Improve database initialization by moving it from `internal/model` to `internal/database`. - Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool. - Enhance error handling and security by implementing fail-fast checks for encryption key length during startup. - Clean up unused code, including the `avatar` package and several unused DTOs.
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
|
||
}
|