diff --git a/CHANGELOG.md b/CHANGELOG.md index c769165666e47b6c6b98545d0c8d103713b492b1..1efbc3e5346f88d49c63785cc627bd6ad250c463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ 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-2021-29453](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. + ### Added * Added support for structured logging (JSON). @@ -15,6 +22,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Turned color-coded logs off by default. This can be changed in the config. +### Fixed + +* Fixed memory exhaustion when thumbnailing maliciously crafted images. + ## [1.2.6] - March 25th, 2021 ### Added diff --git a/common/config/conf_domain.go b/common/config/conf_domain.go index 88ff8d2aec9a5fd1aa667e1095dcaa09a7532d27..a3fb44fe8536e124882bf765a0d5f49379610f49 100644 --- a/common/config/conf_domain.go +++ b/common/config/conf_domain.go @@ -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, diff --git a/common/config/conf_main.go b/common/config/conf_main.go index d6b5dc4fceb055bfd642712a4063cac011d3f6fd..3d255c56eadaace116842afca7bc54e8461b057d 100644 --- a/common/config/conf_main.go +++ b/common/config/conf_main.go @@ -90,6 +90,7 @@ func NewDefaultMainConfig() MainRepoConfig { ThumbnailsConfig: ThumbnailsConfig{ MaxSourceBytes: 10485760, // 10mb MaxAnimateSizeBytes: 10485760, // 10mb + MaxPixels: 32000000, // 32M AllowAnimated: true, DefaultAnimated: false, StillFrame: 0.5, diff --git a/common/config/models_domain.go b/common/config/models_domain.go index caaf72867515dd29c0b6f3922f1805e9e6f74706..fbc49152229fc6294d0d9b5537a7757e2d5e7f6b 100644 --- a/common/config/models_domain.go +++ b/common/config/models_domain.go @@ -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"` diff --git a/config.sample.yaml b/config.sample.yaml index 7c9150c13735521652ec166bf9eaad1b5ae407ea..e2aca61847814258b4de4357ea4bee1139517727 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -360,6 +360,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. # diff --git a/thumbnailing/i/01-factories.go b/thumbnailing/i/01-factories.go index 6ae368bf3cabe61d51c92f1066976d8c21ab6f3c..bc0501cb8d487fd1a3367422589acc0bc8afe9d3 100644 --- a/thumbnailing/i/01-factories.go +++ b/thumbnailing/i/01-factories.go @@ -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 { diff --git a/thumbnailing/i/apng.go b/thumbnailing/i/apng.go index 13915692926f51ca77f6bc13efb7f6a1456b07a8..8dcd19672afe5b612c0b785f66bd936c7d0468e6 100644 --- a/thumbnailing/i/apng.go +++ b/thumbnailing/i/apng.go @@ -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) diff --git a/thumbnailing/i/flac.go b/thumbnailing/i/flac.go index 1c45f6e5892a31de3c34d11333c82f7b5cf616da..15b0050103badeba53aba02f4f410061540d38df 100644 --- a/thumbnailing/i/flac.go +++ b/thumbnailing/i/flac.go @@ -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 { diff --git a/thumbnailing/i/gif.go b/thumbnailing/i/gif.go index dbaee1a51fd31e28086b37ff0c16f026fe55b1e9..f863824513600d5223e8bddddac9338bc72d9f90 100644 --- a/thumbnailing/i/gif.go +++ b/thumbnailing/i/gif.go @@ -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 { diff --git a/thumbnailing/i/heif.go b/thumbnailing/i/heif.go index ebb290d93f1272ad32e95087cc4446c1b0a63110..9a088aa1f215260bcb2055e6bcd2e940289a7e54 100644 --- a/thumbnailing/i/heif.go +++ b/thumbnailing/i/heif.go @@ -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) } diff --git a/thumbnailing/i/jpg.go b/thumbnailing/i/jpg.go index d166b0505a59d59d39d658cdaabb81d4368b1251..2a0226697cdb82c34d45c33b4e035988a1df341d 100644 --- a/thumbnailing/i/jpg.go +++ b/thumbnailing/i/jpg.go @@ -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 { diff --git a/thumbnailing/i/mp3.go b/thumbnailing/i/mp3.go index efc678232dc6b95466fe2c550faa196fd0a89c45..70f91204517967f7bf7e6c24697db654db3ceebb 100644 --- a/thumbnailing/i/mp3.go +++ b/thumbnailing/i/mp3.go @@ -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 { diff --git a/thumbnailing/i/mp4.go b/thumbnailing/i/mp4.go index 89648b1260c489fcb4bf4f0c2fc1d83753dd52e8..689db9202e8c758eb678d17e38792cf7c032380c 100644 --- a/thumbnailing/i/mp4.go +++ b/thumbnailing/i/mp4.go @@ -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 { diff --git a/thumbnailing/i/ogg.go b/thumbnailing/i/ogg.go index 8b73cb41891dbc2613a5e2915dade50aca8e2325..14cabb4b26bc4a9a9a39b47280c17a70d1645db1 100644 --- a/thumbnailing/i/ogg.go +++ b/thumbnailing/i/ogg.go @@ -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 { diff --git a/thumbnailing/i/png.go b/thumbnailing/i/png.go index a48f2382dbda70cced283e87a1e3b038f689c7d1..bb92306871014aad1eb3bf7c68c09ae4b736bad2 100644 --- a/thumbnailing/i/png.go +++ b/thumbnailing/i/png.go @@ -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 { diff --git a/thumbnailing/i/svg.go b/thumbnailing/i/svg.go index cb833dc55a25df76024331632b6b6db6ba4d22a2..e7f8cafcf3177291127447accea52d180ea2a024 100644 --- a/thumbnailing/i/svg.go +++ b/thumbnailing/i/svg.go @@ -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 { diff --git a/thumbnailing/i/wav.go b/thumbnailing/i/wav.go index 265fc28fdfdf2334fd41dbb98b82e9978b4f0a3a..435341b80abe372b14eabcc0d0f5713c57fdb085 100644 --- a/thumbnailing/i/wav.go +++ b/thumbnailing/i/wav.go @@ -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 { diff --git a/thumbnailing/i/webp.go b/thumbnailing/i/webp.go index d492d820190490ca14cca51df2210ebffe397637..cd09c530c6a6e6235a95f141e1c5647477d5d872 100644 --- a/thumbnailing/i/webp.go +++ b/thumbnailing/i/webp.go @@ -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" @@ -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 { diff --git a/thumbnailing/thumbnail.go b/thumbnailing/thumbnail.go index 422374de097e2738198a84af8cc41290170b89d9..0f54122f870d391cdd02dd8b699cc54cec3e7acd 100644 --- a/thumbnailing/thumbnail.go +++ b/thumbnailing/thumbnail.go @@ -2,6 +2,7 @@ package thumbnailing import ( "errors" + "github.com/turt2live/matrix-media-repo/common" "io" "io/ioutil" "reflect" @@ -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) }