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)
 }