diff --git a/go_backend/cover.go b/go_backend/cover.go index 24f7888e..e6818ecd 100644 --- a/go_backend/cover.go +++ b/go_backend/cover.go @@ -36,6 +36,7 @@ func downloadCoverToMemory(coverURL string) ([]byte, error) { const ( embeddedCoverJPEGQuality = 88 + maxCoverDownloadBytes = 24 * 1024 * 1024 // Decoding arbitrary provider artwork allocates roughly four bytes per // pixel. Refuse pathological images before Decode so a malicious extension // cannot force an unbounded mobile allocation. Normal artwork through @@ -298,11 +299,17 @@ func fetchCoverBytes(downloadURL string) ([]byte, error) { if resp.StatusCode != 200 { return nil, fmt.Errorf("cover download failed: HTTP %d", resp.StatusCode) } + if resp.ContentLength > maxCoverDownloadBytes { + return nil, fmt.Errorf("cover download exceeds %d MiB limit", maxCoverDownloadBytes/(1024*1024)) + } - data, err := io.ReadAll(resp.Body) + data, err := io.ReadAll(io.LimitReader(resp.Body, maxCoverDownloadBytes+1)) if err != nil { return nil, fmt.Errorf("failed to read cover data: %w", err) } + if len(data) > maxCoverDownloadBytes { + return nil, fmt.Errorf("cover download exceeds %d MiB limit", maxCoverDownloadBytes/(1024*1024)) + } width, height := coverDimensions(data) GoLog("[Cover] Downloaded %d KB (%dx%d)", len(data)/1024, width, height) diff --git a/go_backend/cover_test.go b/go_backend/cover_test.go index 4fcc8ec4..31131559 100644 --- a/go_backend/cover_test.go +++ b/go_backend/cover_test.go @@ -2,10 +2,14 @@ package gobackend import ( "bytes" + "fmt" "image" "image/color" "image/jpeg" "image/png" + "net/http" + "net/http/httptest" + "strings" "sync" "sync/atomic" "testing" @@ -151,6 +155,20 @@ func TestDownloadCoverUsesProviderURLUnchanged(t *testing.T) { } } +func TestFetchCoverBytesRejectsOversizedResponse(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + writer.Header().Set("Content-Length", fmt.Sprintf("%d", maxCoverDownloadBytes+1)) + writer.WriteHeader(http.StatusOK) + })) + defer server.Close() + SetAllowPrivateNetwork(true) + defer SetAllowPrivateNetwork(false) + + if _, err := fetchCoverBytes(server.URL); err == nil || !strings.Contains(err.Error(), "exceeds") { + t.Fatalf("expected oversized cover rejection, got %v", err) + } +} + func TestResizeCoverForEmbeddingPreservesAspectRatio(t *testing.T) { original := encodedTestCover(t, 1200, 600, "jpeg")