Browse Source

Security processing options

DarthSim 2 years ago
parent
commit
9416168575

+ 4 - 0
config/config.go

@@ -42,6 +42,7 @@ var (
 	MaxAnimationFrameResolution int
 	MaxSvgCheckBytes            int
 	MaxRedirects                int
+	AllowSecurityOptions        bool
 
 	JpegProgressive       bool
 	PngInterlaced         bool
@@ -226,6 +227,7 @@ func Reset() {
 	MaxAnimationFrameResolution = 0
 	MaxSvgCheckBytes = 32 * 1024
 	MaxRedirects = 10
+	AllowSecurityOptions = false
 
 	JpegProgressive = false
 	PngInterlaced = false
@@ -405,6 +407,8 @@ func Configure() error {
 
 	configurators.Bool(&SanitizeSvg, "IMGPROXY_SANITIZE_SVG")
 
+	configurators.Bool(&AllowSecurityOptions, "IMGPROXY_ALLOW_SECURITY_OPTIONS")
+
 	configurators.Bool(&JpegProgressive, "IMGPROXY_JPEG_PROGRESSIVE")
 	configurators.Bool(&PngInterlaced, "IMGPROXY_PNG_INTERLACED")
 	configurators.Bool(&PngQuantize, "IMGPROXY_PNG_QUANTIZE")

+ 4 - 0
docs/configuration.md

@@ -114,6 +114,10 @@ Also you may want imgproxy to respond with the same error message that it writes
 
 * `IMGPROXY_DEVELOPMENT_ERRORS_MODE`: when true, imgproxy will respond with detailed error messages. Not recommended for production because some errors may contain stack traces.
 
+* `IMGPROXY_ALLOW_SECURITY_OPTIONS`: when `true`, allows usage of security-related processing options such as `max_src_resolution`, `max_src_file_size`, `max_animation_frames`, and `max_animation_frame_resolution`. Default: `false`.
+
+**⚠️Warning:** `IMGPROXY_ALLOW_SECURITY_OPTIONS` allows bypassing your security restrictions. Don't set it to `true` unless you are completely sure that an attacker can't change your imgproxy URLs.
+
 ## Cookies
 
 imgproxy can pass cookies in image requests. This can be activated with `IMGPROXY_COOKIE_PASSTHROUGH`. Unfortunately the `Cookie` header doesn't contain information about which URLs these cookies are applicable to, so imgproxy can only assume (or must be told).

+ 44 - 0
docs/generating_the_url.md

@@ -795,6 +795,50 @@ Read more about presets in the [Presets](presets.md) guide.
 
 Default: empty
 
+### Max src resolution
+
+```
+max_src_resolution:%resolution
+msr:%resolution
+```
+
+Allows redefining `IMGPROXY_MAX_SRC_RESOLUTION` config.
+
+**⚠️Warning:** Since this option allows redefining a security restriction, its usage is not allowed unless the `IMGPROXY_ALLOW_SECURITY_OPTIONS` config is set to `true`.
+
+### Max src file size
+
+```
+max_src_file_size:%size
+msfs:%size
+```
+
+Allows redefining `IMGPROXY_MAX_SRC_FILE_SIZE` config.
+
+**⚠️Warning:** Since this option allows redefining a security restriction, its usage is not allowed unless the `IMGPROXY_ALLOW_SECURITY_OPTIONS` config is set to `true`.
+
+### Max animation frames
+
+```
+max_animation_frames:%size
+maf:%size
+```
+
+Allows redefining `IMGPROXY_MAX_ANIMATION_FRAMES` config.
+
+**⚠️Warning:** Since this option allows redefining a security restriction, its usage is not allowed unless the `IMGPROXY_ALLOW_SECURITY_OPTIONS` config is set to `true`.
+
+### Max animation frame resolution
+
+```
+max_animation_frame_resolution:%size
+mafr:%size
+```
+
+Allows redefining `IMGPROXY_MAX_ANIMATION_FRAME_RESOLUTION` config.
+
+**⚠️Warning:** Since this option allows redefining a security restriction, its usage is not allowed unless the `IMGPROXY_ALLOW_SECURITY_OPTIONS` config is set to `true`.
+
 ## Source URL
 ### Plain
 

+ 3 - 2
imagedata/download.go

@@ -11,6 +11,7 @@ import (
 
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
+	"github.com/imgproxy/imgproxy/v3/security"
 
 	azureTransport "github.com/imgproxy/imgproxy/v3/transport/azure"
 	fsTransport "github.com/imgproxy/imgproxy/v3/transport/fs"
@@ -205,7 +206,7 @@ func requestImage(imageURL string, header http.Header, jar *cookiejar.Jar) (*htt
 	return res, nil
 }
 
-func download(imageURL string, header http.Header, jar *cookiejar.Jar) (*ImageData, error) {
+func download(imageURL string, header http.Header, jar *cookiejar.Jar, secopts security.Options) (*ImageData, error) {
 	// We use this for testing
 	if len(redirectAllRequestsTo) > 0 {
 		imageURL = redirectAllRequestsTo
@@ -234,7 +235,7 @@ func download(imageURL string, header http.Header, jar *cookiejar.Jar) (*ImageDa
 		contentLength = 0
 	}
 
-	imgdata, err := readAndCheckImage(body, contentLength)
+	imgdata, err := readAndCheckImage(body, contentLength, secopts)
 	if err != nil {
 		return nil, ierrors.Wrap(err, 0)
 	}

+ 13 - 12
imagedata/image_data.go

@@ -13,6 +13,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
+	"github.com/imgproxy/imgproxy/v3/security"
 )
 
 var (
@@ -61,17 +62,17 @@ func Init() error {
 
 func loadWatermark() (err error) {
 	if len(config.WatermarkData) > 0 {
-		Watermark, err = FromBase64(config.WatermarkData, "watermark")
+		Watermark, err = FromBase64(config.WatermarkData, "watermark", security.DefaultOptions())
 		return
 	}
 
 	if len(config.WatermarkPath) > 0 {
-		Watermark, err = FromFile(config.WatermarkPath, "watermark")
+		Watermark, err = FromFile(config.WatermarkPath, "watermark", security.DefaultOptions())
 		return
 	}
 
 	if len(config.WatermarkURL) > 0 {
-		Watermark, err = Download(config.WatermarkURL, "watermark", nil, nil)
+		Watermark, err = Download(config.WatermarkURL, "watermark", nil, nil, security.DefaultOptions())
 		return
 	}
 
@@ -81,11 +82,11 @@ func loadWatermark() (err error) {
 func loadFallbackImage() (err error) {
 	switch {
 	case len(config.FallbackImageData) > 0:
-		FallbackImage, err = FromBase64(config.FallbackImageData, "fallback image")
+		FallbackImage, err = FromBase64(config.FallbackImageData, "fallback image", security.DefaultOptions())
 	case len(config.FallbackImagePath) > 0:
-		FallbackImage, err = FromFile(config.FallbackImagePath, "fallback image")
+		FallbackImage, err = FromFile(config.FallbackImagePath, "fallback image", security.DefaultOptions())
 	case len(config.FallbackImageURL) > 0:
-		FallbackImage, err = Download(config.FallbackImageURL, "fallback image", nil, nil)
+		FallbackImage, err = Download(config.FallbackImageURL, "fallback image", nil, nil, security.DefaultOptions())
 	default:
 		FallbackImage, err = nil, nil
 	}
@@ -100,11 +101,11 @@ func loadFallbackImage() (err error) {
 	return err
 }
 
-func FromBase64(encoded, desc string) (*ImageData, error) {
+func FromBase64(encoded, desc string, secopts security.Options) (*ImageData, error) {
 	dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encoded))
 	size := 4 * (len(encoded)/3 + 1)
 
-	imgdata, err := readAndCheckImage(dec, size)
+	imgdata, err := readAndCheckImage(dec, size, secopts)
 	if err != nil {
 		return nil, fmt.Errorf("Can't decode %s: %s", desc, err)
 	}
@@ -112,7 +113,7 @@ func FromBase64(encoded, desc string) (*ImageData, error) {
 	return imgdata, nil
 }
 
-func FromFile(path, desc string) (*ImageData, error) {
+func FromFile(path, desc string, secopts security.Options) (*ImageData, error) {
 	f, err := os.Open(path)
 	if err != nil {
 		return nil, fmt.Errorf("Can't read %s: %s", desc, err)
@@ -123,7 +124,7 @@ func FromFile(path, desc string) (*ImageData, error) {
 		return nil, fmt.Errorf("Can't read %s: %s", desc, err)
 	}
 
-	imgdata, err := readAndCheckImage(f, int(fi.Size()))
+	imgdata, err := readAndCheckImage(f, int(fi.Size()), secopts)
 	if err != nil {
 		return nil, fmt.Errorf("Can't read %s: %s", desc, err)
 	}
@@ -131,8 +132,8 @@ func FromFile(path, desc string) (*ImageData, error) {
 	return imgdata, nil
 }
 
-func Download(imageURL, desc string, header http.Header, jar *cookiejar.Jar) (*ImageData, error) {
-	imgdata, err := download(imageURL, header, jar)
+func Download(imageURL, desc string, header http.Header, jar *cookiejar.Jar, secopts security.Options) (*ImageData, error) {
+	imgdata, err := download(imageURL, header, jar, secopts)
 	if err != nil {
 		if nmErr, ok := err.(*ErrorNotModified); ok {
 			nmErr.Message = fmt.Sprintf("Can't download %s: %s", desc, nmErr.Message)

+ 6 - 28
imagedata/read.go

@@ -13,10 +13,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/security"
 )
 
-var (
-	ErrSourceFileTooBig            = ierrors.New(422, "Source image file is too big", "Invalid source image")
-	ErrSourceImageTypeNotSupported = ierrors.New(422, "Source image type not supported", "Invalid source image")
-)
+var ErrSourceImageTypeNotSupported = ierrors.New(422, "Source image type not supported", "Invalid source image")
 
 var downloadBufPool *bufpool.Pool
 
@@ -24,34 +21,15 @@ func initRead() {
 	downloadBufPool = bufpool.New("download", config.Concurrency, config.DownloadBufferSize)
 }
 
-type hardLimitReader struct {
-	r    io.Reader
-	left int
-}
-
-func (lr *hardLimitReader) Read(p []byte) (n int, err error) {
-	if lr.left <= 0 {
-		return 0, ErrSourceFileTooBig
-	}
-	if len(p) > lr.left {
-		p = p[0:lr.left]
-	}
-	n, err = lr.r.Read(p)
-	lr.left -= n
-	return
-}
-
-func readAndCheckImage(r io.Reader, contentLength int) (*ImageData, error) {
-	if config.MaxSrcFileSize > 0 && contentLength > config.MaxSrcFileSize {
-		return nil, ErrSourceFileTooBig
+func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options) (*ImageData, error) {
+	if err := security.CheckFileSize(contentLength, secopts); err != nil {
+		return nil, err
 	}
 
 	buf := downloadBufPool.Get(contentLength, false)
 	cancel := func() { downloadBufPool.Put(buf) }
 
-	if config.MaxSrcFileSize > 0 {
-		r = &hardLimitReader{r: r, left: config.MaxSrcFileSize}
-	}
+	r = security.LimitFileSize(r, secopts)
 
 	br := bufreader.New(r, buf)
 
@@ -67,7 +45,7 @@ func readAndCheckImage(r io.Reader, contentLength int) (*ImageData, error) {
 		return nil, checkTimeoutErr(err)
 	}
 
-	if err = security.CheckDimensions(meta.Width(), meta.Height(), 1); err != nil {
+	if err = security.CheckDimensions(meta.Width(), meta.Height(), 1, secopts); err != nil {
 		buf.Reset()
 		cancel()
 		return nil, err

+ 86 - 0
options/processing_options.go

@@ -14,6 +14,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/imath"
+	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/structdiff"
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
@@ -108,6 +109,8 @@ type ProcessingOptions struct {
 
 	UsedPresets []string
 
+	SecurityOptions security.Options
+
 	defaultQuality int
 }
 
@@ -143,6 +146,8 @@ func NewProcessingOptions() *ProcessingOptions {
 		SkipProcessingFormats: append([]imagetype.Type(nil), config.SkipProcessingFormats...),
 		UsedPresets:           make([]string, 0, len(config.Presets)),
 
+		SecurityOptions: security.DefaultOptions(),
+
 		// Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed
 		defaultQuality: config.Quality,
 	}
@@ -884,6 +889,78 @@ func applyReturnAttachmentOption(po *ProcessingOptions, args []string) error {
 	return nil
 }
 
+func applyMaxSrcResolutionOption(po *ProcessingOptions, args []string) error {
+	if err := security.IsSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	if len(args) > 1 {
+		return fmt.Errorf("Invalid max_src_resolution arguments: %v", args)
+	}
+
+	if x, err := strconv.ParseFloat(args[0], 64); err == nil && x > 0 {
+		po.SecurityOptions.MaxSrcResolution = int(x * 1000000)
+	} else {
+		return fmt.Errorf("Invalid max_src_resolution: %s", args[0])
+	}
+
+	return nil
+}
+
+func applyMaxSrcFileSizeOption(po *ProcessingOptions, args []string) error {
+	if err := security.IsSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	if len(args) > 1 {
+		return fmt.Errorf("Invalid max_src_file_size arguments: %v", args)
+	}
+
+	if x, err := strconv.Atoi(args[0]); err == nil {
+		po.SecurityOptions.MaxSrcFileSize = x
+	} else {
+		return fmt.Errorf("Invalid max_src_file_size: %s", args[0])
+	}
+
+	return nil
+}
+
+func applyMaxAnimationFramesOption(po *ProcessingOptions, args []string) error {
+	if err := security.IsSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	if len(args) > 1 {
+		return fmt.Errorf("Invalid max_animation_frames arguments: %v", args)
+	}
+
+	if x, err := strconv.Atoi(args[0]); err == nil && x > 0 {
+		po.SecurityOptions.MaxAnimationFrames = x
+	} else {
+		return fmt.Errorf("Invalid max_animation_frames: %s", args[0])
+	}
+
+	return nil
+}
+
+func applyMaxAnimationFrameResolutionOption(po *ProcessingOptions, args []string) error {
+	if err := security.IsSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	if len(args) > 1 {
+		return fmt.Errorf("Invalid max_animation_frame_resolution arguments: %v", args)
+	}
+
+	if x, err := strconv.ParseFloat(args[0], 64); err == nil {
+		po.SecurityOptions.MaxAnimationFrameResolution = int(x * 1000000)
+	} else {
+		return fmt.Errorf("Invalid max_animation_frame_resolution: %s", args[0])
+	}
+
+	return nil
+}
+
 func applyURLOption(po *ProcessingOptions, name string, args []string) error {
 	switch name {
 	case "resize", "rs":
@@ -965,6 +1042,15 @@ func applyURLOption(po *ProcessingOptions, name string, args []string) error {
 	// Presets
 	case "preset", "pr":
 		return applyPresetOption(po, args)
+	// Security
+	case "max_src_resolution", "msr":
+		return applyMaxSrcResolutionOption(po, args)
+	case "max_src_file_size", "msfs":
+		return applyMaxSrcFileSizeOption(po, args)
+	case "max_animation_frames", "maf":
+		return applyMaxAnimationFramesOption(po, args)
+	case "max_animation_frame_resolution", "mafr":
+		return applyMaxAnimationFrameResolutionOption(po, args)
 	}
 
 	return fmt.Errorf("Unknown processing option: %s", name)

+ 1 - 1
processing/processing.go

@@ -120,7 +120,7 @@ func transformAnimated(ctx context.Context, img *vips.Image, po *options.Process
 	framesCount := imath.Min(img.Height()/frameHeight, config.MaxAnimationFrames)
 
 	// Double check dimensions because animated image has many frames
-	if err = security.CheckDimensions(imgWidth, frameHeight, framesCount); err != nil {
+	if err = security.CheckDimensions(imgWidth, frameHeight, framesCount, po.SecurityOptions); err != nil {
 		return err
 	}
 

+ 1 - 1
processing_handler.go

@@ -295,7 +295,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 			checkErr(ctx, "download", err)
 		}
 
-		return imagedata.Download(imageURL, "source image", imgRequestHeader, cookieJar)
+		return imagedata.Download(imageURL, "source image", imgRequestHeader, cookieJar, po.SecurityOptions)
 	}()
 
 	if err == nil {

+ 42 - 0
security/file_size.go

@@ -0,0 +1,42 @@
+package security
+
+import (
+	"io"
+
+	"github.com/imgproxy/imgproxy/v3/ierrors"
+)
+
+var ErrSourceFileTooBig = ierrors.New(422, "Source image file is too big", "Invalid source image")
+
+type hardLimitReader struct {
+	r    io.Reader
+	left int
+}
+
+func (lr *hardLimitReader) Read(p []byte) (n int, err error) {
+	if lr.left <= 0 {
+		return 0, ErrSourceFileTooBig
+	}
+	if len(p) > lr.left {
+		p = p[0:lr.left]
+	}
+	n, err = lr.r.Read(p)
+	lr.left -= n
+	return
+}
+
+func CheckFileSize(size int, opts Options) error {
+	if opts.MaxSrcFileSize > 0 && size > opts.MaxSrcFileSize {
+		return ErrSourceFileTooBig
+	}
+
+	return nil
+}
+
+func LimitFileSize(r io.Reader, opts Options) io.Reader {
+	if opts.MaxSrcFileSize > 0 {
+		return &hardLimitReader{r: r, left: opts.MaxSrcFileSize}
+	}
+
+	return r
+}

+ 4 - 5
security/image_size.go

@@ -1,7 +1,6 @@
 package security
 
 import (
-	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imath"
 )
@@ -9,15 +8,15 @@ import (
 var ErrSourceResolutionTooBig = ierrors.New(422, "Source image resolution is too big", "Invalid source image")
 var ErrSourceFrameResolutionTooBig = ierrors.New(422, "Source image frame resolution is too big", "Invalid source image")
 
-func CheckDimensions(width, height, frames int) error {
+func CheckDimensions(width, height, frames int, opts Options) error {
 	frames = imath.Max(frames, 1)
 
-	if frames > 1 && config.MaxAnimationFrameResolution > 0 {
-		if width*height > config.MaxAnimationFrameResolution {
+	if frames > 1 && opts.MaxAnimationFrameResolution > 0 {
+		if width*height > opts.MaxAnimationFrameResolution {
 			return ErrSourceFrameResolutionTooBig
 		}
 	} else {
-		if width*height*frames > config.MaxSrcResolution {
+		if width*height*frames > opts.MaxSrcResolution {
 			return ErrSourceResolutionTooBig
 		}
 	}

+ 32 - 0
security/options.go

@@ -0,0 +1,32 @@
+package security
+
+import (
+	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/ierrors"
+)
+
+var ErrSecurityOptionsNotAllowed = ierrors.New(403, "Security processing options are not allowed", "Invalid URL")
+
+type Options struct {
+	MaxSrcResolution            int
+	MaxSrcFileSize              int
+	MaxAnimationFrames          int
+	MaxAnimationFrameResolution int
+}
+
+func DefaultOptions() Options {
+	return Options{
+		MaxSrcResolution:            config.MaxSrcResolution,
+		MaxSrcFileSize:              config.MaxSrcFileSize,
+		MaxAnimationFrames:          config.MaxAnimationFrames,
+		MaxAnimationFrameResolution: config.MaxAnimationFrameResolution,
+	}
+}
+
+func IsSecurityOptionsAllowed() error {
+	if config.AllowSecurityOptions {
+		return nil
+	}
+
+	return ErrSecurityOptionsNotAllowed
+}