fix(05): WR-01 bound thumb dimensions and decoded image size

This commit is contained in:
Jakub Zych
2026-09-19 15:30:58 +02:00
parent 44126cc935
commit c211822448
2 changed files with 36 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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()