feat: enhance attachment handling with MIME type validation

This commit is contained in:
Johnny 2025-12-17 08:58:43 +08:00
parent 642271a831
commit ea14280cb3
2 changed files with 59 additions and 4 deletions

View File

@ -64,6 +64,9 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
if request.Attachment.Type == "" { if request.Attachment.Type == "" {
return nil, status.Errorf(codes.InvalidArgument, "type is required") return nil, status.Errorf(codes.InvalidArgument, "type is required")
} }
if !isValidMimeType(request.Attachment.Type) {
return nil, status.Errorf(codes.InvalidArgument, "invalid MIME type format")
}
// Use provided attachment_id or generate a new one // Use provided attachment_id or generate a new one
attachmentUID := request.AttachmentId attachmentUID := request.AttachmentId
@ -457,3 +460,15 @@ func validateFilename(filename string) bool {
return true return true
} }
func isValidMimeType(mimeType string) bool {
// Reject empty or excessively long MIME types
if mimeType == "" || len(mimeType) > 255 {
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
}

View File

@ -118,15 +118,39 @@ func (s *FileServerService) serveAttachmentFile(c echo.Context) error {
contentType += "; charset=utf-8" contentType += "; charset=utf-8"
} }
// Prevent XSS attacks by serving potentially unsafe files as octet-stream // Prevent XSS attacks by serving potentially unsafe files as octet-stream
if strings.EqualFold(contentType, "image/svg+xml") || unsafeTypes := []string{
strings.EqualFold(contentType, "text/html") || "text/html",
strings.EqualFold(contentType, "application/xhtml+xml") { "text/javascript",
contentType = "application/octet-stream" "application/javascript",
"application/x-javascript",
"text/xml",
"application/xml",
"application/xhtml+xml",
"image/svg+xml",
}
for _, unsafeType := range unsafeTypes {
if strings.EqualFold(contentType, unsafeType) {
contentType = "application/octet-stream"
break
}
} }
// Set common headers // Set common headers
c.Response().Header().Set("Content-Type", contentType) c.Response().Header().Set("Content-Type", contentType)
c.Response().Header().Set("Cache-Control", "public, max-age=3600") c.Response().Header().Set("Cache-Control", "public, max-age=3600")
// Prevent MIME-type sniffing which could lead to XSS
c.Response().Header().Set("X-Content-Type-Options", "nosniff")
// Defense-in-depth: prevent embedding in frames and restrict content loading
c.Response().Header().Set("X-Frame-Options", "DENY")
c.Response().Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline';")
// Force download for non-media files to prevent XSS execution
if !strings.HasPrefix(contentType, "image/") &&
!strings.HasPrefix(contentType, "video/") &&
!strings.HasPrefix(contentType, "audio/") &&
contentType != "application/pdf" {
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", attachment.Filename))
}
// For video/audio: Use http.ServeContent for automatic range request support // For video/audio: Use http.ServeContent for automatic range request support
// This is critical for Safari which REQUIRES range request support // This is critical for Safari which REQUIRES range request support
@ -169,6 +193,18 @@ func (s *FileServerService) serveUserAvatar(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to extract image info").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "failed to extract image info").SetInternal(err)
} }
// Validate avatar MIME type to prevent XSS
allowedAvatarTypes := map[string]bool{
"image/png": true,
"image/jpeg": true,
"image/jpg": true,
"image/gif": true,
"image/webp": true,
}
if !allowedAvatarTypes[imageType] {
return echo.NewHTTPError(http.StatusBadRequest, "invalid avatar image type")
}
// Decode base64 data // Decode base64 data
imageData, err := base64.StdEncoding.DecodeString(base64Data) imageData, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil { if err != nil {
@ -178,6 +214,10 @@ func (s *FileServerService) serveUserAvatar(c echo.Context) error {
// Set cache headers for avatars // Set cache headers for avatars
c.Response().Header().Set("Content-Type", imageType) c.Response().Header().Set("Content-Type", imageType)
c.Response().Header().Set("Cache-Control", "public, max-age=3600") c.Response().Header().Set("Cache-Control", "public, max-age=3600")
c.Response().Header().Set("X-Content-Type-Options", "nosniff")
// Defense-in-depth: prevent embedding in frames
c.Response().Header().Set("X-Frame-Options", "DENY")
c.Response().Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline';")
return c.Blob(http.StatusOK, imageType, imageData) return c.Blob(http.StatusOK, imageType, imageData)
} }