fix: normalize attachment MIME types before validation

This commit is contained in:
boojack 2026-04-06 14:05:24 +08:00
parent aafcc21ae6
commit c3e7e2c316
2 changed files with 42 additions and 15 deletions

View File

@ -82,24 +82,25 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
if !validateFilename(request.Attachment.Filename) {
return nil, status.Errorf(codes.InvalidArgument, "filename contains invalid characters or format")
}
if request.Attachment.Type == "" {
normalizedMimeType := request.Attachment.Type
if normalizedMimeType == "" {
ext := filepath.Ext(request.Attachment.Filename)
mimeType := mime.TypeByExtension(ext)
if mimeType == "" {
mimeType = http.DetectContentType(request.Attachment.Content)
}
// ParseMediaType to strip parameters
mediaType, _, err := mime.ParseMediaType(mimeType)
if err == nil {
request.Attachment.Type = mediaType
if normalizedType, ok := normalizeMimeType(mimeType); ok {
normalizedMimeType = normalizedType
}
}
if request.Attachment.Type == "" {
request.Attachment.Type = "application/octet-stream"
if normalizedMimeType == "" {
normalizedMimeType = "application/octet-stream"
}
if !isValidMimeType(request.Attachment.Type) {
normalizedType, ok := normalizeMimeType(normalizedMimeType)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "invalid MIME type format")
}
request.Attachment.Type = normalizedType
attachmentUID, err := ValidateAndGenerateUID(request.AttachmentId)
if err != nil {
@ -617,16 +618,18 @@ func validateFilename(filename string) bool {
return true
}
func isValidMimeType(mimeType string) bool {
// Reject empty or excessively long MIME types
func normalizeMimeType(mimeType string) (string, bool) {
mimeType = strings.TrimSpace(mimeType)
if mimeType == "" || len(mimeType) > 255 {
return false
return "", false
}
// MIME type must match the pattern: type/subtype
// Allow common characters in MIME types per RFC 2045
matched, _ := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}$`, mimeType)
return matched
mediaType, _, err := mime.ParseMediaType(mimeType)
if err != nil || mediaType == "" || len(mediaType) > 255 {
return "", false
}
return mediaType, true
}
func (s *APIV1Service) validateAttachmentFilter(ctx context.Context, filterStr string) error {

View File

@ -61,6 +61,30 @@ func TestCreateAttachment(t *testing.T) {
require.Equal(t, "application/octet-stream", attachment.Type)
})
t.Run("Type_WithParameters_NormalizedBeforeValidation", func(t *testing.T) {
attachment, err := ts.Service.CreateAttachment(userCtx, &v1pb.CreateAttachmentRequest{
Attachment: &v1pb.Attachment{
Filename: "voice-note.webm",
Type: "audio/webm;codecs=opus",
Content: []byte("fake webm content"),
},
})
require.NoError(t, err)
require.Equal(t, "audio/webm", attachment.Type)
})
t.Run("Type_InvalidFormat_Rejected", func(t *testing.T) {
_, err := ts.Service.CreateAttachment(userCtx, &v1pb.CreateAttachmentRequest{
Attachment: &v1pb.Attachment{
Filename: "broken.webm",
Type: `audio/webm;codecs="unterminated`,
Content: []byte("fake webm content"),
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid MIME type format")
})
t.Run("LocalStorage_PathCollisionUsesUniqueReference", func(t *testing.T) {
_, err := ts.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_STORAGE,