Parcourir la source

fix(icon): reject oversized favicons

Frédéric Guillot il y a 3 semaines
Parent
commit
7e5219e5ac
2 fichiers modifiés avec 101 ajouts et 0 suppressions
  1. 24 0
      internal/reader/icon/finder.go
  2. 77 0
      internal/reader/icon/finder_test.go

+ 24 - 0
internal/reader/icon/finder.go

@@ -184,6 +184,11 @@ func (f *iconFinder) downloadIcon(iconURL string) (*model.Icon, error) {
 }
 
 func resizeIcon(icon *model.Icon) *model.Icon {
+	const (
+		maxFaviconDimension = 4096
+		maxFaviconPixels    = 4096 * 4096
+	)
+
 	if icon.MimeType == "image/svg+xml" {
 		minifier := minify.New()
 		minifier.AddFunc("image/svg+xml", svg.Minify)
@@ -209,6 +214,25 @@ func resizeIcon(icon *model.Icon) *model.Icon {
 		slog.Warn("Unable to decode icon metadata", slog.Any("error", err))
 		return icon
 	}
+
+	if config.Width <= 0 || config.Height <= 0 {
+		slog.Warn("Icon resize skipped: invalid image dimensions",
+			slog.Int("width", config.Width),
+			slog.Int("height", config.Height),
+		)
+		return icon
+	}
+
+	pixelCount := int64(config.Width) * int64(config.Height)
+	if config.Width > maxFaviconDimension || config.Height > maxFaviconDimension || pixelCount > maxFaviconPixels {
+		slog.Warn("Icon rejected: image dimensions are too large",
+			slog.Int("width", config.Width),
+			slog.Int("height", config.Height),
+			slog.Int64("pixel_count", pixelCount),
+		)
+		return nil
+	}
+
 	if config.Height <= 32 && config.Width <= 32 {
 		slog.Debug("Icon doesn't need to be resized", slog.Int("height", config.Height), slog.Int("width", config.Width))
 		return icon

+ 77 - 0
internal/reader/icon/finder_test.go

@@ -6,6 +6,8 @@ package icon // import "miniflux.app/v2/internal/reader/icon"
 import (
 	"bytes"
 	"encoding/base64"
+	"encoding/binary"
+	"hash/crc32"
 	"image"
 	"strings"
 	"testing"
@@ -408,6 +410,28 @@ func TestResizeInvalidImage(t *testing.T) {
 	}
 }
 
+func TestResizeIconTooLargeDimensions(t *testing.T) {
+	icon := model.Icon{
+		Content:  mustMinimalPNG(t, 4097, 7),
+		MimeType: "image/png",
+	}
+
+	if resizeIcon(&icon) != nil {
+		t.Fatalf("Should reject images with too large dimensions")
+	}
+}
+
+func TestResizeIconTooLargePixelCount(t *testing.T) {
+	icon := model.Icon{
+		Content:  mustMinimalPNG(t, 4096, 4097),
+		MimeType: "image/png",
+	}
+
+	if resizeIcon(&icon) != nil {
+		t.Fatalf("Should reject images with too many pixels")
+	}
+}
+
 func TestMinifySvg(t *testing.T) {
 	data := []byte(`<svg path d=" M1 4h-.001 V1h2v.001 M1 2.6 h1v.001"/></svg>`)
 	want := []byte(`<svg path="" d="M1 4H.999V1h2v.001M1 2.6h1v.001"/></svg>`)
@@ -441,3 +465,56 @@ func TestMinifySvgWithError(t *testing.T) {
 		t.Fatalf("Expected MimeType to remain image/svg+xml, got %s", result.MimeType)
 	}
 }
+
+func mustMinimalPNG(t *testing.T, width, height uint32) []byte {
+	t.Helper()
+
+	var b bytes.Buffer
+	b.Write([]byte{137, 80, 78, 71, 13, 10, 26, 10})
+	writePNGChunk(t, &b, "IHDR", func(data []byte) {
+		binary.BigEndian.PutUint32(data[0:4], width)
+		binary.BigEndian.PutUint32(data[4:8], height)
+		data[8] = 8
+		data[9] = 2
+	})
+	writePNGChunk(t, &b, "IEND", nil)
+
+	return b.Bytes()
+}
+
+func writePNGChunk(t *testing.T, b *bytes.Buffer, chunkType string, fill func([]byte)) {
+	t.Helper()
+
+	dataLen := 0
+	if chunkType == "IHDR" {
+		dataLen = 13
+	}
+
+	if err := binary.Write(b, binary.BigEndian, uint32(dataLen)); err != nil {
+		t.Fatal(err)
+	}
+
+	if _, err := b.WriteString(chunkType); err != nil {
+		t.Fatal(err)
+	}
+
+	data := make([]byte, dataLen)
+	if fill != nil {
+		fill(data)
+	}
+	if _, err := b.Write(data); err != nil {
+		t.Fatal(err)
+	}
+
+	crc := crc32.NewIEEE()
+	if _, err := crc.Write([]byte(chunkType)); err != nil {
+		t.Fatal(err)
+	}
+	if _, err := crc.Write(data); err != nil {
+		t.Fatal(err)
+	}
+
+	if err := binary.Write(b, binary.BigEndian, crc.Sum32()); err != nil {
+		t.Fatal(err)
+	}
+}