From c211822448eae8f90485a11723e27f06d4456325 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sat, 19 Sep 2026 15:30:58 +0200 Subject: [PATCH] fix(05): WR-01 bound thumb dimensions and decoded image size --- lagoon/attach/thumb.go | 15 ++++++++++++++- lagoon/attach/thumb_test.go | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lagoon/attach/thumb.go b/lagoon/attach/thumb.go index 282aeeb..33d5057 100644 --- a/lagoon/attach/thumb.go +++ b/lagoon/attach/thumb.go @@ -16,6 +16,12 @@ import ( "gocloud.dev/blob" ) +const ( + maxThumbEdge = 4096 + maxThumbSourceBytes = 32 << 20 + maxThumbSourcePixels = 4096 * 4096 +) + // thumbToken is the alphabet allowed for the mode and extension segments of a // thumb filename. Both are interpolated into a blob key, which fileblob maps // to a filesystem path, so separators and dots must never reach it. @@ -110,6 +116,9 @@ func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode st if !thumbToken.MatchString(mode) { return "", fmt.Errorf("attach: invalid thumb mode %q", mode) } + if w <= 0 || h <= 0 || w > maxThumbEdge || h > maxThumbEdge { + return "", fmt.Errorf("attach: thumb size %dx%d is out of range", w, h) + } ext := fileExt(f.DiskName) if !thumbToken.MatchString(ext) { return "", fmt.Errorf("attach: invalid thumb extension %q", ext) @@ -129,7 +138,7 @@ func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode st if err != nil { return "", fmt.Errorf("attach: read original: %w", err) } - src, _, err := image.Decode(r) + src, _, err := image.Decode(io.LimitReader(r, maxThumbSourceBytes)) closeErr := r.Close() if err != nil { return "", fmt.Errorf("attach: decode original: %w", err) @@ -137,6 +146,10 @@ func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode st if closeErr != nil { return "", closeErr } + bounds := src.Bounds() + if int64(bounds.Dx())*int64(bounds.Dy()) > maxThumbSourcePixels { + return "", fmt.Errorf("attach: original image is too large") + } resized := resizeImage(src, w, h, mode) contentType := "image/jpeg" switch ext { diff --git a/lagoon/attach/thumb_test.go b/lagoon/attach/thumb_test.go index 4f29efb..ba28e44 100644 --- a/lagoon/attach/thumb_test.go +++ b/lagoon/attach/thumb_test.go @@ -43,6 +43,28 @@ func TestThumbFilenameRejectsUnsafeTokens(t *testing.T) { } } +func TestFileThumbRejectsOutOfRangeSize(t *testing.T) { + bucket := memblob.OpenBucket(nil) + defer bucket.Close() + f := &File{ID: 1, DiskName: "abc123xyz.jpg"} + for _, tc := range []struct{ w, h int }{ + {0, 200}, + {200, 0}, + {-1, -1}, + {maxThumbEdge + 1, 200}, + {200, maxThumbEdge + 1}, + {100000, 100000}, + } { + if _, err := f.Thumb(t.Context(), bucket, tc.w, tc.h, "crop"); err == nil { + t.Fatalf("size %dx%d must be rejected", tc.w, tc.h) + } + } + iter := bucket.List(nil) + if obj, err := iter.Next(t.Context()); err != io.EOF { + t.Fatalf("rejected sizes must not touch the bucket, found %v (err %v)", obj, err) + } +} + func TestFileThumbRejectsTraversalMode(t *testing.T) { bucket := memblob.OpenBucket(nil) defer bucket.Close()