diff --git a/README.md b/README.md
index 1a4740a2d..576af3b0c 100644
--- a/README.md
+++ b/README.md
@@ -196,6 +196,7 @@ The following libraries and frameworks are used by GoToSocial, with gratitude 
   - [gin-contrib/static](https://github.com/gin-contrib/static); Gin static page middleware. [MIT License](https://spdx.org/licenses/MIT.html)
 - [go-fed/httpsig](https://github.com/go-fed/httpsig); secure HTTP signature library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
 - [google/uuid](https://github.com/google/uuid); UUID generation. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html)
+- [google/wuffs](https://github.com/google/wuffs); png-stripping code. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html).
 - [go-playground/validator](https://github.com/go-playground/validator); struct validation. [MIT License](https://spdx.org/licenses/MIT.html)
 - [gorilla/websocket](https://github.com/gorilla/websocket); Websocket connectivity. [BSD-2-Clause License](https://spdx.org/licenses/BSD-2-Clause.html).
 - [gruf/go-runners](https://codeberg.org/gruf/go-runners); worker pool library. [MIT License](https://spdx.org/licenses/MIT.html).
diff --git a/internal/media/image.go b/internal/media/image.go
index 4f2db0e95..cb26ad539 100644
--- a/internal/media/image.go
+++ b/internal/media/image.go
@@ -74,7 +74,7 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) {
 	case mimeImageJpeg:
 		i, err = jpeg.Decode(r)
 	case mimeImagePng:
-		i, err = png.Decode(r)
+		i, err = StrippedPngDecode(r)
 	default:
 		err = fmt.Errorf("content type %s not recognised", contentType)
 	}
@@ -117,7 +117,7 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima
 	case mimeImageJpeg:
 		i, err = jpeg.Decode(r)
 	case mimeImagePng:
-		i, err = png.Decode(r)
+		i, err = StrippedPngDecode(r)
 	case mimeImageGif:
 		i, err = gif.Decode(r)
 	default:
@@ -175,7 +175,7 @@ func deriveStaticEmoji(r io.Reader, contentType string) (*imageMeta, error) {
 
 	switch contentType {
 	case mimeImagePng:
-		i, err = png.Decode(r)
+		i, err = StrippedPngDecode(r)
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/media/manager_test.go b/internal/media/manager_test.go
index 95cefe1db..75e706ba7 100644
--- a/internal/media/manager_test.go
+++ b/internal/media/manager_test.go
@@ -111,6 +111,150 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
 	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 }
 
+func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
+	ctx := context.Background()
+
+	data := func(_ context.Context) (io.Reader, int, error) {
+		// load bytes from a test image
+		b, err := os.ReadFile("./test/test-png-noalphachannel.png")
+		if err != nil {
+			panic(err)
+		}
+		return bytes.NewBuffer(b), len(b), nil
+	}
+
+	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
+
+	// process the media with no additional info provided
+	processingMedia, err := suite.manager.ProcessMedia(ctx, data, nil, accountID, nil)
+	suite.NoError(err)
+	// fetch the attachment id from the processing media
+	attachmentID := processingMedia.AttachmentID()
+
+	// do a blocking call to fetch the attachment
+	attachment, err := processingMedia.LoadAttachment(ctx)
+	suite.NoError(err)
+	suite.NotNil(attachment)
+
+	// make sure it's got the stuff set on it that we expect
+	// the attachment ID and accountID we expect
+	suite.Equal(attachmentID, attachment.ID)
+	suite.Equal(accountID, attachment.AccountID)
+
+	// file meta should be correctly derived from the image
+	suite.EqualValues(gtsmodel.Original{
+		Width: 186, Height: 187, Size: 34782, Aspect: 0.9946524064171123,
+	}, attachment.FileMeta.Original)
+	suite.EqualValues(gtsmodel.Small{
+		Width: 186, Height: 187, Size: 34782, Aspect: 0.9946524064171123,
+	}, attachment.FileMeta.Small)
+	suite.Equal("image/png", attachment.File.ContentType)
+	suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
+	suite.Equal(17471, attachment.File.FileSize)
+	suite.Equal("LFP?{^.A-?xd.9o#RVRQ~oj:_0xW", attachment.Blurhash)
+
+	// now make sure the attachment is in the database
+	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+	suite.NoError(err)
+	suite.NotNil(dbAttachment)
+
+	// make sure the processed file is in storage
+	processedFullBytes, err := suite.storage.Get(attachment.File.Path)
+	suite.NoError(err)
+	suite.NotEmpty(processedFullBytes)
+
+	// load the processed bytes from our test folder, to compare
+	processedFullBytesExpected, err := os.ReadFile("./test/test-png-noalphachannel-processed.png")
+	suite.NoError(err)
+	suite.NotEmpty(processedFullBytesExpected)
+
+	// the bytes in storage should be what we expected
+	suite.Equal(processedFullBytesExpected, processedFullBytes)
+
+	// now do the same for the thumbnail and make sure it's what we expected
+	processedThumbnailBytes, err := suite.storage.Get(attachment.Thumbnail.Path)
+	suite.NoError(err)
+	suite.NotEmpty(processedThumbnailBytes)
+
+	processedThumbnailBytesExpected, err := os.ReadFile("./test/test-png-noalphachannel-thumbnail.jpg")
+	suite.NoError(err)
+	suite.NotEmpty(processedThumbnailBytesExpected)
+
+	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
+}
+
+func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
+	ctx := context.Background()
+
+	data := func(_ context.Context) (io.Reader, int, error) {
+		// load bytes from a test image
+		b, err := os.ReadFile("./test/test-png-alphachannel.png")
+		if err != nil {
+			panic(err)
+		}
+		return bytes.NewBuffer(b), len(b), nil
+	}
+
+	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
+
+	// process the media with no additional info provided
+	processingMedia, err := suite.manager.ProcessMedia(ctx, data, nil, accountID, nil)
+	suite.NoError(err)
+	// fetch the attachment id from the processing media
+	attachmentID := processingMedia.AttachmentID()
+
+	// do a blocking call to fetch the attachment
+	attachment, err := processingMedia.LoadAttachment(ctx)
+	suite.NoError(err)
+	suite.NotNil(attachment)
+
+	// make sure it's got the stuff set on it that we expect
+	// the attachment ID and accountID we expect
+	suite.Equal(attachmentID, attachment.ID)
+	suite.Equal(accountID, attachment.AccountID)
+
+	// file meta should be correctly derived from the image
+	suite.EqualValues(gtsmodel.Original{
+		Width: 186, Height: 187, Size: 34782, Aspect: 0.9946524064171123,
+	}, attachment.FileMeta.Original)
+	suite.EqualValues(gtsmodel.Small{
+		Width: 186, Height: 187, Size: 34782, Aspect: 0.9946524064171123,
+	}, attachment.FileMeta.Small)
+	suite.Equal("image/png", attachment.File.ContentType)
+	suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
+	suite.Equal(18904, attachment.File.FileSize)
+	suite.Equal("LFP?{^.A-?xd.9o#RVRQ~oj:_0xW", attachment.Blurhash)
+
+	// now make sure the attachment is in the database
+	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+	suite.NoError(err)
+	suite.NotNil(dbAttachment)
+
+	// make sure the processed file is in storage
+	processedFullBytes, err := suite.storage.Get(attachment.File.Path)
+	suite.NoError(err)
+	suite.NotEmpty(processedFullBytes)
+
+	// load the processed bytes from our test folder, to compare
+	processedFullBytesExpected, err := os.ReadFile("./test/test-png-alphachannel-processed.png")
+	suite.NoError(err)
+	suite.NotEmpty(processedFullBytesExpected)
+
+	// the bytes in storage should be what we expected
+	suite.Equal(processedFullBytesExpected, processedFullBytes)
+
+	// now do the same for the thumbnail and make sure it's what we expected
+	processedThumbnailBytes, err := suite.storage.Get(attachment.Thumbnail.Path)
+	suite.NoError(err)
+	suite.NotEmpty(processedThumbnailBytes)
+
+	processedThumbnailBytesExpected, err := os.ReadFile("./test/test-png-alphachannel-thumbnail.jpg")
+	suite.NoError(err)
+	suite.NotEmpty(processedThumbnailBytesExpected)
+
+	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
+}
+
 func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
 	ctx := context.Background()
 
diff --git a/internal/media/png-stripper.go b/internal/media/png-stripper.go
new file mode 100644
index 000000000..62ae393fe
--- /dev/null
+++ b/internal/media/png-stripper.go
@@ -0,0 +1,204 @@
+/*
+   GoToSocial
+   Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU Affero General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Affero General Public License for more details.
+
+   You should have received a copy of the GNU Affero General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package media
+
+/*
+   The code in this file is taken from the following source:
+   https://github.com/google/wuffs/blob/414a011491ff513b86d8694c5d71800f3cb5a715/script/strip-png-ancillary-chunks.go
+
+   It presents a workaround for this issue: https://github.com/golang/go/issues/43382
+
+   The license for the copied code is reproduced below:
+
+      Copyright 2021 The Wuffs Authors.
+
+      Licensed under the Apache License, Version 2.0 (the "License");
+      you may not use this file except in compliance with the License.
+      You may obtain a copy of the License at
+
+         https://www.apache.org/licenses/LICENSE-2.0
+
+      Unless required by applicable law or agreed to in writing, software
+      distributed under the License is distributed on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+      See the License for the specific language governing permissions and
+      limitations under the License.
+*/
+
+// strip-png-ancillary-chunks.go copies PNG data from stdin to stdout, removing
+// any ancillary chunks.
+//
+// Specification-compliant PNG decoders are required to honor critical chunks
+// but may ignore ancillary (non-critical) chunks. Stripping out ancillary
+// chunks before decoding should mean that different PNG decoders will agree on
+// the decoded output regardless of which ancillary chunk types they choose to
+// honor. Specifically, some PNG decoders may implement color and gamma
+// correction but not all do.
+//
+// This program will strip out all ancillary chunks, but it should be
+// straightforward to copy-paste-and-modify it to strip out only certain chunk
+// types (e.g. only "tRNS" transparency chunks).
+//
+// --------
+//
+// A PNG file consists of an 8-byte magic identifier and then a series of
+// chunks. Each chunk is:
+//
+//  - a 4-byte uint32 payload length N.
+//  - a 4-byte chunk type (e.g. "gAMA" for gamma correction metadata).
+//  - an N-byte payload.
+//  - a 4-byte CRC-32 checksum of the previous (N + 4) bytes, including the
+//    chunk type but excluding the payload length.
+//
+// Chunk types consist of 4 ASCII letters. The upper-case / lower-case bit of
+// the first letter denote critical or ancillary chunks: "IDAT" and "PLTE" are
+// critical, "gAMA" and "tEXt" are ancillary. See
+// https://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-naming-conventions
+//
+// --------
+
+import (
+	"encoding/binary"
+	"image"
+	"image/png"
+	"io"
+)
+
+// chunkTypeAncillaryBit is whether the first byte of a big-endian uint32 chunk
+// type (the first of four ASCII letters) is lower-case.
+const chunkTypeAncillaryBit = 0x20000000
+
+// PNGAncillaryChunkStripper wraps another io.Reader to strip ancillary chunks,
+// if the data is in the PNG file format. If the data isn't PNG, it is passed
+// through unmodified.
+type PNGAncillaryChunkStripper struct {
+	// Reader is the wrapped io.Reader.
+	Reader io.Reader
+
+	// stickyErr is the first error returned from the wrapped io.Reader.
+	stickyErr error
+
+	// buffer[rIndex:wIndex] holds data read from the wrapped io.Reader that
+	// wasn't passed through yet.
+	buffer [8]byte
+	rIndex int
+	wIndex int
+
+	// pending and discard is the number of remaining bytes for (and whether to
+	// discard or pass through) the current chunk-in-progress.
+	pending int64
+	discard bool
+
+	// notPNG is set true if the data stream doesn't start with the 8-byte PNG
+	// magic identifier. If true, the wrapped io.Reader's data (including the
+	// first up-to-8 bytes) is passed through without modification.
+	notPNG bool
+
+	// seenMagic is whether we've seen the 8-byte PNG magic identifier.
+	seenMagic bool
+}
+
+// Read implements io.Reader.
+func (r *PNGAncillaryChunkStripper) Read(p []byte) (int, error) {
+	for {
+		// If the wrapped io.Reader returned a non-nil error, drain r.buffer
+		// (what data we have) and return that error (if fully drained).
+		if r.stickyErr != nil {
+			n := copy(p, r.buffer[r.rIndex:r.wIndex])
+			r.rIndex += n
+			if r.rIndex < r.wIndex {
+				return n, nil
+			}
+			return n, r.stickyErr
+		}
+
+		// Handle trivial requests, including draining our buffer.
+		if len(p) == 0 {
+			return 0, nil
+		} else if r.rIndex < r.wIndex {
+			n := copy(p, r.buffer[r.rIndex:r.wIndex])
+			r.rIndex += n
+			return n, nil
+		}
+
+		// From here onwards, our buffer is drained: r.rIndex == r.wIndex.
+
+		// Handle non-PNG input.
+		if r.notPNG {
+			return r.Reader.Read(p)
+		}
+
+		// Continue processing any PNG chunk that's in progress, whether
+		// discarding it or passing it through.
+		for r.pending > 0 {
+			if int64(len(p)) > r.pending {
+				p = p[:r.pending]
+			}
+			n, err := r.Reader.Read(p)
+			r.pending -= int64(n)
+			r.stickyErr = err
+			if r.discard {
+				continue
+			}
+			return n, err
+		}
+
+		// We're either expecting the 8-byte PNG magic identifier or the 4-byte
+		// PNG chunk length + 4-byte PNG chunk type. Either way, read 8 bytes.
+		r.rIndex = 0
+		r.wIndex, r.stickyErr = io.ReadFull(r.Reader, r.buffer[:8])
+		if r.stickyErr != nil {
+			// Undo io.ReadFull converting io.EOF to io.ErrUnexpectedEOF.
+			if r.stickyErr == io.ErrUnexpectedEOF {
+				r.stickyErr = io.EOF
+			}
+			continue
+		}
+
+		// Process those 8 bytes, either:
+		//  - a PNG chunk (if we've already seen the PNG magic identifier),
+		//  - the PNG magic identifier itself (if the input is a PNG) or
+		//  - something else (if it's not a PNG).
+		//nolint:gocritic
+		if r.seenMagic {
+			// The number of pending bytes is equal to (N + 4) because of the 4
+			// byte trailer, a checksum.
+			r.pending = int64(binary.BigEndian.Uint32(r.buffer[:4])) + 4
+			chunkType := binary.BigEndian.Uint32(r.buffer[4:])
+			r.discard = (chunkType & chunkTypeAncillaryBit) != 0
+			if r.discard {
+				r.rIndex = r.wIndex
+			}
+		} else if string(r.buffer[:8]) == "\x89PNG\x0D\x0A\x1A\x0A" {
+			r.seenMagic = true
+		} else {
+			r.notPNG = true
+		}
+	}
+}
+
+// StrippedPngDecode strips ancillary data from png to allow more lenient decoding of pngs
+// see: https://github.com/golang/go/issues/43382
+// and: https://github.com/google/wuffs/blob/414a011491ff513b86d8694c5d71800f3cb5a715/script/strip-png-ancillary-chunks.go
+func StrippedPngDecode(r io.Reader) (image.Image, error) {
+	strippedPngReader := io.Reader(&PNGAncillaryChunkStripper{
+		Reader: r,
+	})
+	return png.Decode(strippedPngReader)
+}
diff --git a/internal/media/test/test-png-alphachannel-processed.png b/internal/media/test/test-png-alphachannel-processed.png
new file mode 100644
index 000000000..d3b67e66d
Binary files /dev/null and b/internal/media/test/test-png-alphachannel-processed.png differ
diff --git a/internal/media/test/test-png-alphachannel-thumbnail.jpg b/internal/media/test/test-png-alphachannel-thumbnail.jpg
new file mode 100644
index 000000000..ca62f4ea6
Binary files /dev/null and b/internal/media/test/test-png-alphachannel-thumbnail.jpg differ
diff --git a/internal/media/test/test-png-noalphachannel-processed.png b/internal/media/test/test-png-noalphachannel-processed.png
new file mode 100644
index 000000000..46bec9097
Binary files /dev/null and b/internal/media/test/test-png-noalphachannel-processed.png differ
diff --git a/internal/media/test/test-png-noalphachannel-thumbnail.jpg b/internal/media/test/test-png-noalphachannel-thumbnail.jpg
new file mode 100644
index 000000000..ca62f4ea6
Binary files /dev/null and b/internal/media/test/test-png-noalphachannel-thumbnail.jpg differ