This commit is contained in:
zhx 2026-07-22 19:26:25 +08:00
parent f564ecd059
commit 97c7e4ffa2
2 changed files with 26 additions and 19 deletions

View File

@ -102,10 +102,15 @@ func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request
if !ok {
return
}
if isRasterUploadContentType(detectedContentType) {
// 通用文件入口也可能承接房间背景和资料图片。只要文件头能确定为受支持的
// 栅格图片,就使用真实字节类型覆盖客户端 MIME后续对象后缀也据此规范化。
contentType = detectedContentType
}
if policy.RequireImage {
// 头像用文件头嗅探结果做安全判断,不信任客户端传入的 multipart Content-Type。
contentType = detectedContentType
if !avatarExtensionMatchesContentType(header.Filename, detectedContentType) || animatedWebPHeader(head) {
if animatedWebPHeader(head) {
// 注册和旧头像入口只允许静态图片;动态 WebP 的 MIME 与静态相同,必须读取 VP8X 动画位拦截。
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
@ -190,15 +195,10 @@ func isAvatarContentType(contentType string) bool {
}
}
func avatarExtensionMatchesContentType(filename string, contentType string) bool {
extension := strings.ToLower(path.Ext(filename))
func isRasterUploadContentType(contentType string) bool {
switch strings.ToLower(strings.TrimSpace(contentType)) {
case "image/jpeg":
return extension == ".jpg" || extension == ".jpeg"
case "image/png":
return extension == ".png"
case "image/webp":
return extension == ".webp"
case "image/jpeg", "image/png", "image/webp", "image/gif":
return true
default:
return false
}
@ -227,10 +227,8 @@ func buildUploadObjectKey(policy uploadPolicy, ownerSegment string, filename str
}
func normalizedUploadExtension(filename string, contentType string) string {
if extension := cleanUploadExtension(path.Ext(filename)); extension != "" {
return extension
}
// 受支持图片的规范后缀只能来自嗅探后的真实 Content-Type不能让相册缓存文件名
// 把 WebP/GIF 对象错误保存成 .jpg非图片文件仍保留原始安全后缀。
switch strings.ToLower(strings.TrimSpace(contentType)) {
case "image/jpeg":
return ".jpg"
@ -238,6 +236,14 @@ func normalizedUploadExtension(filename string, contentType string) string {
return ".png"
case "image/webp":
return ".webp"
case "image/gif":
return ".gif"
}
if extension := cleanUploadExtension(path.Ext(filename)); extension != "" {
return extension
}
switch strings.ToLower(strings.TrimSpace(contentType)) {
case "application/pdf":
return ".pdf"
default:

View File

@ -11597,13 +11597,14 @@ func TestVoiceRoomRoutesRejectWrongHTTPMethod(t *testing.T) {
}
}
func TestUploadAvatarAllowsIncompleteProfileAndWritesObject(t *testing.T) {
func TestUploadAvatarUsesDetectedTypeAndAllowsIncompleteProfile(t *testing.T) {
uploader := &fakeObjectUploader{}
handler := NewHandler(&fakeRoomClient{})
handler.SetObjectUploader(uploader)
router := handler.Routes(auth.NewVerifier("secret"))
payload := append([]byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}, bytes.Repeat([]byte{1}, 32)...)
body, contentType := multipartUploadBody(t, "file", "avatar.png", "image/png", payload)
payload := append([]byte{'R', 'I', 'F', 'F', 0x24, 0, 0, 0, 'W', 'E', 'B', 'P', 'V', 'P', '8', ' '}, bytes.Repeat([]byte{1}, 32)...)
// Android 相册缓存可能把 WebP/GIF 标成 JPEG上传对象必须以文件头识别结果命名。
body, contentType := multipartUploadBody(t, "file", "avatar.jpg", "image/jpeg", payload)
request := httptest.NewRequest(http.MethodPost, "/api/v1/files/avatar/upload", body)
request.Header.Set("Content-Type", contentType)
request.Header.Set("Authorization", "Bearer "+signGatewayTokenWithProfile(t, "secret", 42, false))
@ -11623,13 +11624,13 @@ func TestUploadAvatarAllowsIncompleteProfileAndWritesObject(t *testing.T) {
if response.Code != httpkit.CodeOK || !ok {
t.Fatalf("unexpected upload envelope: %+v", response)
}
if !strings.HasPrefix(uploader.lastKey, "app/avatars/42/") || !strings.HasSuffix(uploader.lastKey, ".png") {
if !strings.HasPrefix(uploader.lastKey, "app/avatars/42/") || !strings.HasSuffix(uploader.lastKey, ".webp") {
t.Fatalf("unexpected avatar object key: %q", uploader.lastKey)
}
if string(uploader.lastContent) != string(payload) || uploader.lastSizeBytes != int64(len(payload)) || uploader.lastContentType != "image/png" {
if string(uploader.lastContent) != string(payload) || uploader.lastSizeBytes != int64(len(payload)) || uploader.lastContentType != "image/webp" {
t.Fatalf("uploaded object mismatch: key=%s size=%d type=%s body=%x", uploader.lastKey, uploader.lastSizeBytes, uploader.lastContentType, uploader.lastContent)
}
if data["url"] != "https://media.example.com/"+uploader.lastKey || data["object_key"] != uploader.lastKey || data["content_type"] != "image/png" {
if data["url"] != "https://media.example.com/"+uploader.lastKey || data["object_key"] != uploader.lastKey || data["content_type"] != "image/webp" {
t.Fatalf("unexpected upload data: %+v", data)
}
}