mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
fix(artwork): reject decompression-bomb dimensions before decoding
This commit is contained in:
parent
b3526c0fba
commit
d6434b9929
@ -38,6 +38,10 @@ const thumbnailSize = 128
|
||||
// point at an arbitrarily large endpoint, and 20MB is generous for any real cover.
|
||||
const maxImageBytes = 20 << 20
|
||||
|
||||
// maxImagePixels caps declared dimensions: a tiny compressed file can declare a
|
||||
// huge canvas that image.Decode would expand into gigabytes (decompression bomb).
|
||||
const maxImagePixels = 64 << 20
|
||||
|
||||
// workerDeps are the collaborators processItem needs; extGate is set by NewWorker in
|
||||
// production and nil only in tests, where resolveItem falls back to a plain passthrough.
|
||||
type workerDeps struct {
|
||||
@ -147,6 +151,9 @@ func decodeArtwork(ctx context.Context, hash string, data []byte) (*model.Artwor
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode image config: %w", err)
|
||||
}
|
||||
if int64(cfg.Width)*int64(cfg.Height) > maxImagePixels {
|
||||
return nil, fmt.Errorf("image dimensions %dx%d exceed pixel cap %d", cfg.Width, cfg.Height, maxImagePixels)
|
||||
}
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode image: %w", err)
|
||||
|
||||
@ -2,7 +2,9 @@ package artwork
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -16,6 +18,21 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// pngHeaderWithDims builds just a PNG signature + IHDR chunk declaring w×h. DecodeConfig
|
||||
// reads the header without touching pixel data, so the body can be omitted entirely.
|
||||
func pngHeaderWithDims(w, h uint32) []byte {
|
||||
ihdr := make([]byte, 13)
|
||||
binary.BigEndian.PutUint32(ihdr[0:], w)
|
||||
binary.BigEndian.PutUint32(ihdr[4:], h)
|
||||
ihdr[8] = 8 // bit depth
|
||||
ihdr[9] = 2 // color type: truecolor
|
||||
chunk := append([]byte("IHDR"), ihdr...)
|
||||
out := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}
|
||||
out = binary.BigEndian.AppendUint32(out, uint32(len(ihdr)))
|
||||
out = append(out, chunk...)
|
||||
return binary.BigEndian.AppendUint32(out, crc32.ChecksumIEEE(chunk))
|
||||
}
|
||||
|
||||
var _ = Describe("processItem", func() {
|
||||
var (
|
||||
ctx context.Context
|
||||
@ -229,6 +246,13 @@ var _ = Describe("processItem", func() {
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
It("decompression bomb: rejects huge declared dimensions before the full decode", func() {
|
||||
data := pngHeaderWithDims(50000, 50000) // 2.5 gigapixels, far above the cap
|
||||
_, err := decodeArtwork(ctx, "bomb", data)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("dimensions"))
|
||||
})
|
||||
|
||||
It("store write failure: fails without writing state", func() {
|
||||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{
|
||||
{ID: "al7", Name: "Album", EmbedArtPath: "tests/fixtures/artist/an-album/test.mp3", FolderIDs: []string{"f1"}},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user