Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,8 @@ Usage of imagor:
VIPS avif speed, the lowest is at 0 and the fastest is at 9 (Default 5).
-vips-strip-metadata
VIPS strips all metadata from the resulting image
-vips-unlimited
VIPS bypass image max resolution check and remove all denial of service limits

-sentry-dsn
include sentry dsn to integrate imagor with sentry
Expand Down
3 changes: 3 additions & 0 deletions config/vipsconfig/vipsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ func WithVips(fs *flag.FlagSet, cb func() (*zap.Logger, bool)) imagor.Option {
"VIPS avif speed, the lowest is at 0 and the fastest is at 9 (Default 5).")
vipsStripMetadata = fs.Bool("vips-strip-metadata", false,
"VIPS strips all metadata from the resulting image")
vipsUnlimited = fs.Bool("vips-unlimited", false,
"VIPS bypass image max resolution check and remove all denial of service limits")

logger, isDebug = cb()
)
Expand All @@ -57,6 +59,7 @@ func WithVips(fs *flag.FlagSet, cb func() (*zap.Logger, bool)) imagor.Option {
vipsprocessor.WithMozJPEG(*vipsMozJPEG),
vipsprocessor.WithAvifSpeed(*vipsAvifSpeed),
vipsprocessor.WithStripMetadata(*vipsStripMetadata),
vipsprocessor.WithUnlimited(*vipsUnlimited),
vipsprocessor.WithLogger(logger),
vipsprocessor.WithDebug(isDebug),
),
Expand Down
26 changes: 20 additions & 6 deletions processor/vipsprocessor/fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,34 @@ import (
// FallbackFunc vips.Image fallback handler when vips.NewImageFromSource failed
type FallbackFunc func(blob *imagor.Blob, options *vips.LoadOptions) (*vips.Image, error)

// BufferFallbackFunc load image from buffer FallbackFunc
func BufferFallbackFunc(blob *imagor.Blob, options *vips.LoadOptions) (*vips.Image, error) {
// bufferFallbackFunc load image from buffer FallbackFunc
func bufferFallbackFunc(blob *imagor.Blob, options *vips.LoadOptions) (*vips.Image, error) {
buf, err := blob.ReadAll()
if err != nil {
return nil, err
}
return vips.NewImageFromBuffer(buf, options)
}

func loadImageFromBMP(r io.Reader) (*vips.Image, error) {
func estimateMaxBMPFileSize(maxResolution int64) int64 {
const (
bmpHeaderSize = 54
bytesPerPixel = 4 // 32-bit RGBA (worst case)
safetyMargin = 1.2 // 20% buffer
)
return int64(float64(bmpHeaderSize+maxResolution*bytesPerPixel) * safetyMargin)
}

func (v *Processor) loadImageFromBMP(r io.Reader) (*vips.Image, error) {
img, err := bmp.Decode(r)
if err != nil {
return nil, err
}
rect := img.Bounds()
size := rect.Size()
if !v.Unlimited && (size.X > v.MaxWidth || size.Y > v.MaxHeight || size.X*size.Y > v.MaxResolution) {
return nil, imagor.ErrMaxResolutionExceeded
}
rgba, ok := img.(*image.RGBA)
if !ok {
rgba = image.NewRGBA(rect)
Expand All @@ -36,17 +48,19 @@ func loadImageFromBMP(r io.Reader) (*vips.Image, error) {
return vips.NewImageFromMemory(rgba.Pix, size.X, size.Y, 4)
}

func BmpFallbackFunc(blob *imagor.Blob, _ *vips.LoadOptions) (*vips.Image, error) {
func (v *Processor) bmpFallbackFunc(blob *imagor.Blob, _ *vips.LoadOptions) (*vips.Image, error) {
if blob.BlobType() == imagor.BlobTypeBMP {
// fallback with Go BMP decoder if vips error on BMP
if blob.Size() > estimateMaxBMPFileSize(int64(v.MaxResolution)) {
return nil, imagor.ErrMaxResolutionExceeded
}
r, _, err := blob.NewReader()
if err != nil {
return nil, err
}
defer func() {
_ = r.Close()
}()
return loadImageFromBMP(r)
return v.loadImageFromBMP(r)
} else {
return nil, imagor.ErrUnsupportedFormat
}
Expand Down
8 changes: 4 additions & 4 deletions processor/vipsprocessor/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,13 @@ func WithMaxResolution(res int) Option {
// WithForceBmpFallback force with BMP fallback
func WithForceBmpFallback() Option {
return func(v *Processor) {
v.FallbackFunc = BmpFallbackFunc
v.FallbackFunc = v.bmpFallbackFunc
}
}

// WithForceBufferFallback force with buffer fallback
func WithForceBufferFallback() Option {
// WithUnlimited with unlimited option that remove all denial of service limits
func WithUnlimited(unlimited bool) Option {
return func(v *Processor) {
v.FallbackFunc = BufferFallbackFunc
v.Unlimited = unlimited
}
}
2 changes: 2 additions & 0 deletions processor/vipsprocessor/option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestWithOption(t *testing.T) {
WithDebug(true),
WithMaxAnimationFrames(3),
WithDisableFilters("rgb", "fill, watermark"),
WithUnlimited(true),
WithForceBmpFallback(),
WithFilter("noop", func(ctx context.Context, img *vips.Image, load imagor.LoadFunc, args ...string) (err error) {
return nil
Expand All @@ -42,6 +43,7 @@ func TestWithOption(t *testing.T) {
assert.Equal(t, 3, v.MaxAnimationFrames)
assert.Equal(t, true, v.MozJPEG)
assert.Equal(t, true, v.StripMetadata)
assert.Equal(t, true, v.Unlimited)
assert.Equal(t, 9, v.AvifSpeed)
assert.Equal(t, []string{"rgb", "fill", "watermark"}, v.DisableFilters)
assert.NotNil(t, v.FallbackFunc)
Expand Down
4 changes: 4 additions & 0 deletions processor/vipsprocessor/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,10 @@ func supportedSaveFormat(format vips.ImageType) vips.ImageType {
func (v *Processor) export(
image *vips.Image, format vips.ImageType, compression int, quality int, palette bool, bitdepth int, stripMetadata bool,
) ([]byte, error) {
// check resolution before export
if _, err := v.CheckResolution(image, nil); err != nil {
return nil, err
}
switch format {
case vips.ImageTypePng:
opts := &vips.PngsaveBufferOptions{
Expand Down
22 changes: 12 additions & 10 deletions processor/vipsprocessor/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Processor struct {
MozJPEG bool
StripMetadata bool
AvifSpeed int
Unlimited bool
Debug bool

disableFilters map[string]bool
Expand Down Expand Up @@ -124,10 +125,10 @@ func (v *Processor) Startup(_ context.Context) error {
}
if v.FallbackFunc == nil {
if vips.HasOperation("magickload_buffer") {
v.FallbackFunc = BufferFallbackFunc
v.FallbackFunc = bufferFallbackFunc
v.Logger.Debug("source fallback", zap.String("fallback", "magickload_buffer"))
} else {
v.FallbackFunc = BmpFallbackFunc
v.FallbackFunc = v.bmpFallbackFunc
v.Logger.Debug("source fallback", zap.String("fallback", "bmp"))
}
}
Expand Down Expand Up @@ -206,6 +207,7 @@ func (v *Processor) NewThumbnail(
if dpi > 0 {
options.Dpi = dpi
}
options.Unlimited = v.Unlimited
var err error
var img *vips.Image
if isMultiPage(blob, n, page) {
Expand Down Expand Up @@ -271,14 +273,14 @@ func (v *Processor) newThumbnailFallback(

// NewImage creates new Image from imagor.Blob
func (v *Processor) NewImage(ctx context.Context, blob *imagor.Blob, n, page int, dpi int) (*vips.Image, error) {
var params = &vips.LoadOptions{}
var options = &vips.LoadOptions{}
if dpi > 0 {
params.Dpi = dpi
options.Dpi = dpi
}
params.FailOnError = false
options.Unlimited = v.Unlimited
if isMultiPage(blob, n, page) {
applyMultiPageOptions(params, n, page)
img, err := v.CheckResolution(v.newImageFromBlob(ctx, blob, params))
applyMultiPageOptions(options, n, page)
img, err := v.CheckResolution(v.newImageFromBlob(ctx, blob, options))
if err != nil {
return nil, WrapErr(err)
}
Expand All @@ -289,7 +291,7 @@ func (v *Processor) NewImage(ctx context.Context, blob *imagor.Blob, n, page int
}
return img, nil
}
img, err := v.CheckResolution(v.newImageFromBlob(ctx, blob, params))
img, err := v.CheckResolution(v.newImageFromBlob(ctx, blob, options))
if err != nil {
return nil, WrapErr(err)
}
Expand Down Expand Up @@ -376,8 +378,8 @@ func (v *Processor) CheckResolution(img *vips.Image, err error) (*vips.Image, er
if err != nil || img == nil {
return img, err
}
if img.Width() > v.MaxWidth || img.PageHeight() > v.MaxHeight ||
(img.Width()*img.Height()) > v.MaxResolution {
if !v.Unlimited && (img.Width() > v.MaxWidth || img.PageHeight() > v.MaxHeight ||
(img.Width()*img.Height()) > v.MaxResolution) {
img.Close()
return nil, imagor.ErrMaxResolutionExceeded
}
Expand Down
46 changes: 46 additions & 0 deletions processor/vipsprocessor/processor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ func TestProcessor(t *testing.T) {
http.MethodGet, "/unsafe/gopher-front.png", nil))
assert.Equal(t, 200, w.Code)

w = httptest.NewRecorder()
app.ServeHTTP(w, httptest.NewRequest(
http.MethodGet, "/unsafe/1000x1000/gopher-front.png", nil))
assert.Equal(t, 422, w.Code)

w = httptest.NewRecorder()
app.ServeHTTP(w, httptest.NewRequest(
http.MethodGet, "/unsafe/gopher.png", nil))
Expand All @@ -317,6 +322,47 @@ func TestProcessor(t *testing.T) {
http.MethodGet, "/unsafe/trim/1000x0/gopher-front.png", nil))
assert.Equal(t, 422, w.Code)
})

t.Run("resolution exceeded bmp", func(t *testing.T) {
app := imagor.New(
imagor.WithLoaders(filestorage.New(testDataDir)),
imagor.WithUnsafe(true),
imagor.WithDebug(true),
imagor.WithLogger(zap.NewExample()),
imagor.WithProcessors(NewProcessor(
WithMaxResolution(150*150),
WithDebug(true),
)),
)
require.NoError(t, app.Startup(context.Background()))
t.Cleanup(func() {
assert.NoError(t, app.Shutdown(context.Background()))
})
w := httptest.NewRecorder()
app.ServeHTTP(w, httptest.NewRequest(
http.MethodGet, "/unsafe/100x100/bmp_24.bmp", nil))
assert.Equal(t, 422, w.Code)
})
t.Run("resolution exceeded bmp 2", func(t *testing.T) {
app := imagor.New(
imagor.WithLoaders(filestorage.New(testDataDir)),
imagor.WithUnsafe(true),
imagor.WithDebug(true),
imagor.WithLogger(zap.NewExample()),
imagor.WithProcessors(NewProcessor(
WithMaxHeight(199),
WithDebug(true),
)),
)
require.NoError(t, app.Startup(context.Background()))
t.Cleanup(func() {
assert.NoError(t, app.Shutdown(context.Background()))
})
w := httptest.NewRecorder()
app.ServeHTTP(w, httptest.NewRequest(
http.MethodGet, "/unsafe/100x100/bmp_24.bmp", nil))
assert.Equal(t, 422, w.Code)
})
t.Run("resolution exceeded max frames within", func(t *testing.T) {
app := imagor.New(
imagor.WithLoaders(filestorage.New(testDataDir)),
Expand Down
Loading