From 93f08628ef4ec79528bbb32ebb4be014115b8915 Mon Sep 17 00:00:00 2001 From: webitel Date: Tue, 26 May 2026 13:46:16 +0300 Subject: [PATCH 1/2] WTEL-9538: Fix MIME-type mismatch --- grpc_api/file.go | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/grpc_api/file.go b/grpc_api/file.go index 556f4d7..fa2bdba 100644 --- a/grpc_api/file.go +++ b/grpc_api/file.go @@ -1,12 +1,17 @@ package grpc_api import ( + "bytes" "context" "errors" "fmt" "io" + "mime" "net/http" "net/url" + "strings" + + "github.com/h2non/filetype" "github.com/webitel/storage/app" @@ -340,7 +345,6 @@ func (api *file) UploadFileUrl(ctx context.Context, in *storage.UploadFileUrlReq fileRequest.DomainId = in.GetDomainId() fileRequest.Name = model.NewId() + "_" + in.GetName() fileRequest.ViewName = model.NewString(in.GetName()) - fileRequest.MimeType = res.Header.Get("Content-Type") fileRequest.Uuid = in.GetUuid() fileRequest.Size = res.ContentLength fileRequest.Channel = model.NewString(channelType(in.Channel)) @@ -350,11 +354,13 @@ func (api *file) UploadFileUrl(ctx context.Context, in *storage.UploadFileUrlReq fileRequest.Uuid = model.NewId() // bad request ? } - if fileRequest.MimeType == "application/octet-stream" && in.Mime != "" { - fileRequest.MimeType = in.Mime + body, mimeType, mimeErr := resolveUrlUploadMime(res, in.Mime) + if mimeErr != nil { + return nil, mimeErr } + fileRequest.MimeType = mimeType - if err = api.ctrl.UploadFileStream(res.Body, &fileRequest); err != nil { + if err = api.ctrl.UploadFileStream(body, &fileRequest); err != nil { return nil, err } @@ -379,6 +385,37 @@ func (api *file) UploadFileUrl(ctx context.Context, in *storage.UploadFileUrlReq return result, nil } +// Remote Content-Type is unreliable (e.g. S3 returns "image" instead of "image/jpeg"), +// so on invalid header we sniff the actual bytes and only trust clientHint as a last resort. +func resolveUrlUploadMime(res *http.Response, clientHint string) (io.ReadCloser, string, model.AppError) { + parsed, _, parseErr := mime.ParseMediaType(res.Header.Get("Content-Type")) + if parseErr == nil && strings.Contains(parsed, "/") && parsed != "application/octet-stream" { + return res.Body, parsed, nil + } + + head := make([]byte, 512) + n, readErr := io.ReadFull(res.Body, head) + if readErr != nil && readErr != io.EOF && readErr != io.ErrUnexpectedEOF { + return nil, "", model.NewInternalError("grpc.upload_file_url.read_head", readErr.Error()) + } + head = head[:n] + body := io.NopCloser(io.MultiReader(bytes.NewReader(head), res.Body)) + + kind, _ := filetype.Match(head) + switch { + case kind != filetype.Unknown: + detected := kind.MIME.Value + if clientHint != "" && clientHint != detected { + return nil, "", model.PolicyErrorExtSuspicious + } + return body, detected, nil + case clientHint != "": + return body, clientHint, nil + default: + return body, "", nil + } +} + func (api *file) DeleteFiles(ctx context.Context, in *storage.DeleteFilesRequest) (*storage.DeleteFilesResponse, error) { session, err := api.ctrl.GetSessionFromCtx(ctx) if err != nil { From fce19ddbd7b0bf3e1dc8954cd93e1e9a0d85bf2e Mon Sep 17 00:00:00 2001 From: webitel Date: Wed, 10 Jun 2026 17:03:08 +0300 Subject: [PATCH 2/2] normalize client MIME hint and fail fast on unknown type --- grpc_api/file.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/grpc_api/file.go b/grpc_api/file.go index fa2bdba..eb9f758 100644 --- a/grpc_api/file.go +++ b/grpc_api/file.go @@ -388,6 +388,14 @@ func (api *file) UploadFileUrl(ctx context.Context, in *storage.UploadFileUrlReq // Remote Content-Type is unreliable (e.g. S3 returns "image" instead of "image/jpeg"), // so on invalid header we sniff the actual bytes and only trust clientHint as a last resort. func resolveUrlUploadMime(res *http.Response, clientHint string) (io.ReadCloser, string, model.AppError) { + if clientHint != "" { + if parsedHint, _, err := mime.ParseMediaType(clientHint); err == nil { + clientHint = parsedHint + } else { + clientHint = "" + } + } + parsed, _, parseErr := mime.ParseMediaType(res.Header.Get("Content-Type")) if parseErr == nil && strings.Contains(parsed, "/") && parsed != "application/octet-stream" { return res.Body, parsed, nil @@ -412,7 +420,7 @@ func resolveUrlUploadMime(res *http.Response, clientHint string) (io.ReadCloser, case clientHint != "": return body, clientHint, nil default: - return body, "", nil + return nil, "", model.PolicyErrorExtUnknown } }