package service import ( "bytes" "image" _ "image/gif" _ "image/jpeg" _ "image/png" "io" "mime" "os" "path/filepath" "strings" "unicode/utf8" "golang.org/x/image/webp" ) // 活跃内容扩展名:下载时强制 application/octet-stream,避免浏览器当页面执行 var activeContentExts = map[string]struct{}{ ".html": {}, ".htm": {}, ".xhtml": {}, ".svg": {}, ".xml": {}, ".js": {}, ".mjs": {}, ".css": {}, ".wasm": {}, } // RasterImageExts 栅格图扩展名(可 DecodeConfig;伪装内容降为 octet-stream) var RasterImageExts = map[string]struct{}{ ".jpg": {}, ".jpeg": {}, ".png": {}, ".webp": {}, ".gif": {}, } const attachmentImageMaxDim = 4096 // IsActiveContentExt 是否为浏览器可能内联执行的扩展名 func IsActiveContentExt(ext string) bool { _, ok := activeContentExts[strings.ToLower(ext)] return ok } // MimeForAttachmentExt 按扩展名映射 MIME;未知或活跃内容走 octet-stream 由调用方再处理 func MimeForAttachmentExt(ext string) string { ext = strings.ToLower(ext) if IsActiveContentExt(ext) { return "application/octet-stream" } if t := mime.TypeByExtension(ext); t != "" { if i := strings.IndexByte(t, ';'); i >= 0 { t = strings.TrimSpace(t[:i]) } return t } return "application/octet-stream" } // looksLikeMarkupOrScript 检测内容是否像 HTML/SVG/XML 伪装(前 512 字节) func looksLikeMarkupOrScript(data []byte) bool { n := len(data) if n > 512 { n = 512 } head := bytes.TrimLeft(data[:n], " \t\r\n") if len(head) == 0 { return false } lower := bytes.ToLower(head) prefixes := [][]byte{ []byte(" 0 && looksLikeMarkupOrScript(head[:n]) { return "application/octet-stream" } if _, err := f.Seek(0, io.SeekStart); err != nil { return "application/octet-stream" } if !validRasterImage(f, ext) { return "application/octet-stream" } return MimeForAttachmentExt(ext) } return MimeForAttachmentExt(ext) } func validRasterImage(r io.Reader, ext string) bool { var cfg image.Config var err error switch ext { case ".webp": cfg, err = webp.DecodeConfig(r) default: cfg, _, err = image.DecodeConfig(r) } if err != nil { return false } if cfg.Width < 1 || cfg.Height < 1 { return false } if cfg.Width > attachmentImageMaxDim || cfg.Height > attachmentImageMaxDim { return false } return true } // sanitizeFilename 清理上传文件名:去路径、危险字符、RTL 伪装、截断 func sanitizeFilename(name string) string { name = filepath.Base(strings.ReplaceAll(name, "\\", "/")) name = strings.TrimSpace(name) name = strings.Map(func(r rune) rune { switch r { case '/', '\\', '\x00', ':', '*', '?', '"', '<', '>', '|', '\u202e', '\u202d', '\u200f', '\u200e', '\ufeff': return '_' default: if r < 0x20 { return '_' } return r } }, name) if utf8.RuneCountInString(name) > 200 { runes := []rune(name) name = string(runes[:200]) } return name } // ExtOfFilename 取小写扩展名(含点),无则空串 func ExtOfFilename(name string) string { return strings.ToLower(filepath.Ext(name)) }