mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
fix(artwork): cap resolved image reads
A user-editable ExternalImageURL can point at an arbitrarily large endpoint; a fast server could make the worker buffer hundreds of MB inside the 5s HTTP timeout. Bound the read to a fixed 20MB cap (no config knob) via io.LimitReader and fail the item if it is exceeded.
This commit is contained in:
parent
87095fab08
commit
67f6d8aee8
@ -31,6 +31,10 @@ const (
|
||||
// thumbnailSize is the max dimension fed to blurhash.
|
||||
const thumbnailSize = 128
|
||||
|
||||
// maxImageBytes caps a resolved image read: a user-editable ExternalImageURL could
|
||||
// point at an arbitrarily large endpoint, and 20MB is generous for any real cover.
|
||||
const maxImageBytes = 20 << 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 {
|
||||
@ -60,11 +64,15 @@ func processItem(ctx context.Context, deps *workerDeps, item model.ArtworkQueueI
|
||||
}
|
||||
defer res.reader.Close()
|
||||
|
||||
data, err := io.ReadAll(res.reader)
|
||||
data, err := io.ReadAll(io.LimitReader(res.reader, maxImageBytes+1))
|
||||
if err != nil {
|
||||
log.Warn(ctx, "artwork: failed to read resolved image", "kind", item.ItemKind, "id", item.ItemID, err)
|
||||
return outcomeFailed
|
||||
}
|
||||
if len(data) > maxImageBytes {
|
||||
log.Warn(ctx, "artwork: resolved image exceeds size cap", "kind", item.ItemKind, "id", item.ItemID, "source", res.source, "cap", maxImageBytes)
|
||||
return outcomeFailed
|
||||
}
|
||||
log.Debug(ctx, "artwork: read resolved image", "kind", item.ItemKind, "id", item.ItemID, "source", res.source, "bytes", len(data))
|
||||
|
||||
hash, err := HashImage(bytes.NewReader(data))
|
||||
|
||||
@ -186,6 +186,27 @@ var _ = Describe("processItem", func() {
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
It("oversized read: a resolved image larger than the cap fails without writing state", func() {
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
conf.Server.DataFolder = conf.NewDir(tmpDir)
|
||||
Expect(os.MkdirAll(filepath.Join(tmpDir, "artwork", "radio"), 0755)).To(Succeed())
|
||||
imgPath := filepath.Join(tmpDir, "artwork", "radio", "big_test.jpg")
|
||||
f, err := os.Create(imgPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(f.Truncate(maxImageBytes + 1)).To(Succeed())
|
||||
Expect(f.Close()).To(Succeed())
|
||||
|
||||
radioRepo := tests.CreateMockedRadioRepo()
|
||||
radioRepo.Data = map[string]*model.Radio{"big": {ID: "big", Name: "Radio", UploadedImage: "big_test.jpg"}}
|
||||
ds.MockedRadio = radioRepo
|
||||
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "ra", ItemID: "big"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err = artRepo.GetItemArtwork("ra", "big", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
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