Skip to content

Commit

Permalink
Fix memory exhaustion from crafted images
Browse files Browse the repository at this point in the history
  • Loading branch information
turt2live committed Apr 16, 2021
1 parent c564b97 commit 96d23d4
Show file tree
Hide file tree
Showing 19 changed files with 91 additions and 1 deletion.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Security advisories

This release includes a fix for [CVE-TBD](https://github.com/turt2live/matrix-media-repo/security/advisories/GHSA-j889-h476-hh9h).

Server administrators are recommended to upgrade as soon as possible. This issue is considered to be exploited in the wild
due to some deployments being affected unexpectedly.

### Fixed

* Fixed memory exhaustion when thumbnailing maliciously crafted images.

## [1.2.6] - March 25th, 2021

### Added
Expand Down
1 change: 1 addition & 0 deletions common/config/conf_domain.go
Expand Up @@ -51,6 +51,7 @@ func NewDefaultDomainConfig() DomainRepoConfig {
Thumbnails: ThumbnailsConfig{
MaxSourceBytes: 10485760, // 10mb
MaxAnimateSizeBytes: 10485760, // 10mb
MaxPixels: 32000000, // 32M
AllowAnimated: true,
DefaultAnimated: false,
StillFrame: 0.5,
Expand Down
1 change: 1 addition & 0 deletions common/config/conf_main.go
Expand Up @@ -88,6 +88,7 @@ func NewDefaultMainConfig() MainRepoConfig {
ThumbnailsConfig: ThumbnailsConfig{
MaxSourceBytes: 10485760, // 10mb
MaxAnimateSizeBytes: 10485760, // 10mb
MaxPixels: 32000000, // 32M
AllowAnimated: true,
DefaultAnimated: false,
StillFrame: 0.5,
Expand Down
1 change: 1 addition & 0 deletions common/config/models_domain.go
Expand Up @@ -37,6 +37,7 @@ type DownloadsConfig struct {

type ThumbnailsConfig struct {
MaxSourceBytes int64 `yaml:"maxSourceBytes"`
MaxPixels int `yaml:"maxPixels"`
Types []string `yaml:"types,flow"`
MaxAnimateSizeBytes int64 `yaml:"maxAnimateSizeBytes"`
Sizes []ThumbnailSize `yaml:"sizes,flow"`
Expand Down
5 changes: 5 additions & 0 deletions config.sample.yaml
Expand Up @@ -352,6 +352,11 @@ thumbnails:
# The maximum number of bytes an image can be before the thumbnailer refuses.
maxSourceBytes: 10485760 # 10MB default, 0 to disable

# The maximum number of pixels an image can have before the thumbnailer refuses. Note that
# this only applies to image types: file types like audio and video are affected solely by
# the maxSourceBytes.
maxPixels: 32000000 # 32M default

# The number of workers to use when generating thumbnails. Raise this number if thumbnails
# are slow to generate or timing out.
#
Expand Down
1 change: 1 addition & 0 deletions thumbnailing/i/01-factories.go
Expand Up @@ -10,6 +10,7 @@ type Generator interface {
supportsAnimation() bool
matches(img []byte, contentType string) bool
GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error)
GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error)
}

type AudioGenerator interface {
Expand Down
8 changes: 8 additions & 0 deletions thumbnailing/i/apng.go
Expand Up @@ -28,6 +28,14 @@ func (d apngGenerator) matches(img []byte, contentType string) bool {
return (contentType == "image/png" && util.IsAnimatedPNG(img)) || contentType == "image/apng"
}

func (d apngGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
i, err := apng.DecodeConfig(bytes.NewBuffer(b))
if err != nil {
return false, 0, 0, err
}
return true, i.Width, i.Height, nil
}

func (d apngGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
if !animated {
return pngGenerator{}.GenerateThumbnail(b, "image/png", width, height, method, false, ctx)
Expand Down
4 changes: 4 additions & 0 deletions thumbnailing/i/flac.go
Expand Up @@ -34,6 +34,10 @@ func (d flacGenerator) decode(b []byte) (beep.StreamSeekCloser, beep.Format, err
return audio, format, nil
}

func (d flacGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
return false, 0, 0, nil
}

func (d flacGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
audio, format, err := d.decode(b)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions thumbnailing/i/gif.go
Expand Up @@ -29,6 +29,10 @@ func (d gifGenerator) matches(img []byte, contentType string) bool {
return contentType == "image/gif"
}

func (d gifGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
return pngGenerator{}.GetOriginDimensions(b, contentType, ctx)
}

func (d gifGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
g, err := gif.DecodeAll(bytes.NewBuffer(b))
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions thumbnailing/i/heif.go
Expand Up @@ -20,6 +20,10 @@ func (d heifGenerator) matches(img []byte, contentType string) bool {
return contentType == "image/heif"
}

func (d heifGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
return pngGenerator{}.GetOriginDimensions(b, contentType, ctx)
}

func (d heifGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
return pngGenerator{}.GenerateThumbnail(b, "image/png", width, height, method, false, ctx)
}
Expand Down
4 changes: 4 additions & 0 deletions thumbnailing/i/jpg.go
Expand Up @@ -28,6 +28,10 @@ func (d jpgGenerator) matches(img []byte, contentType string) bool {
return util.ArrayContains(d.supportedContentTypes(), contentType)
}

func (d jpgGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
return pngGenerator{}.GetOriginDimensions(b, contentType, ctx)
}

func (d jpgGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
src, err := imaging.Decode(bytes.NewBuffer(b))
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions thumbnailing/i/mp3.go
Expand Up @@ -46,6 +46,10 @@ func (d mp3Generator) decode(b []byte) (beep.StreamSeekCloser, beep.Format, erro
return audio, format, nil
}

func (d mp3Generator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
return false, 0, 0, nil
}

func (d mp3Generator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
audio, format, err := d.decode(b)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions thumbnailing/i/mp4.go
Expand Up @@ -28,6 +28,10 @@ func (d mp4Generator) matches(img []byte, contentType string) bool {
return util.ArrayContains(d.supportedContentTypes(), contentType)
}

func (d mp4Generator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
return false, 0, 0, nil
}

func (d mp4Generator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
key, err := util.GenerateRandomString(16)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions thumbnailing/i/ogg.go
Expand Up @@ -34,6 +34,10 @@ func (d oggGenerator) decode(b []byte) (beep.StreamSeekCloser, beep.Format, erro
return audio, format, nil
}

func (d oggGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
return false, 0, 0, nil
}

func (d oggGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
audio, format, err := d.decode(b)
if err != nil {
Expand Down
8 changes: 8 additions & 0 deletions thumbnailing/i/png.go
Expand Up @@ -28,6 +28,14 @@ func (d pngGenerator) matches(img []byte, contentType string) bool {
return contentType == "image/png"
}

func (d pngGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
i, _, err := image.DecodeConfig(bytes.NewBuffer(b))
if err != nil {
return false, 0, 0, err
}
return true, i.Width, i.Height, nil
}

func (d pngGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
src, err := imaging.Decode(bytes.NewBuffer(b))
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions thumbnailing/i/svg.go
Expand Up @@ -28,6 +28,10 @@ func (d svgGenerator) matches(img []byte, contentType string) bool {
return contentType == "image/svg+xml"
}

func (d svgGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
return false, 0, 0, nil
}

func (d svgGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
key, err := util.GenerateRandomString(16)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions thumbnailing/i/wav.go
Expand Up @@ -34,6 +34,10 @@ func (d wavGenerator) decode(b []byte) (beep.StreamSeekCloser, beep.Format, erro
return audio, format, nil
}

func (d wavGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
return false, 0, 0, nil
}

func (d wavGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
audio, format, err := d.decode(b)
if err != nil {
Expand Down
9 changes: 8 additions & 1 deletion thumbnailing/i/webp.go
Expand Up @@ -3,7 +3,6 @@ package i
import (
"bytes"
"errors"

"github.com/turt2live/matrix-media-repo/common/rcontext"
"github.com/turt2live/matrix-media-repo/thumbnailing/m"
"golang.org/x/image/webp"
Expand All @@ -24,6 +23,14 @@ func (d webpGenerator) matches(img []byte, contentType string) bool {
return contentType == "image/webp"
}

func (d webpGenerator) GetOriginDimensions(b []byte, contentType string, ctx rcontext.RequestContext) (bool, int, int, error) {
i, err := webp.DecodeConfig(bytes.NewBuffer(b))
if err != nil {
return false, 0, 0, err
}
return true, i.Width, i.Height, nil
}

func (d webpGenerator) GenerateThumbnail(b []byte, contentType string, width int, height int, method string, animated bool, ctx rcontext.RequestContext) (*m.Thumbnail, error) {
src, err := webp.Decode(bytes.NewBuffer(b))
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions thumbnailing/thumbnail.go
Expand Up @@ -2,6 +2,7 @@ package thumbnailing

import (
"errors"
"github.com/turt2live/matrix-media-repo/common"
"io"
"io/ioutil"
"reflect"
Expand Down Expand Up @@ -40,6 +41,16 @@ func GenerateThumbnail(imgStream io.ReadCloser, contentType string, width int, h
}
ctx.Log.Info("Using generator: ", reflect.TypeOf(generator).Name())

// Validate maximum megapixel values to avoid memory issues
// https://github.com/turt2live/matrix-media-repo/security/advisories/GHSA-j889-h476-hh9h
dimensional, w, h, err := generator.GetOriginDimensions(b, contentType, ctx)
if err != nil {
return nil, err
}
if dimensional && (w * h) >= ctx.Config.Thumbnails.MaxPixels {
return nil, common.ErrMediaTooLarge
}

return generator.GenerateThumbnail(b, contentType, width, height, method, animated, ctx)
}

Expand Down

0 comments on commit 96d23d4

Please sign in to comment.