Browse Source

Add IMGPROXY_ALLOWED_PROCESSING_OPTIONS config

DarthSim 2 weeks ago
parent
commit
2864bab12d
6 changed files with 59 additions and 4 deletions
  1. 1 0
      CHANGELOG.md
  2. 3 0
      config/config.go
  3. 10 0
      options/errors.go
  4. 1 1
      options/presets.go
  5. 9 3
      options/processing_options.go
  6. 35 0
      options/processing_options_test.go

+ 1 - 0
CHANGELOG.md

@@ -3,6 +3,7 @@
 ## [Unreleased]
 ### Added
 - Add [IMGPROXY_MAX_RESULT_DIMENSION](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_MAX_RESULT_DIMENSION) config and [max_result_dimension](https://docs.imgproxy.net/latest/usage/processing#max-result-dimension) processing option.
+- Add [IMGPROXY_ALLOWED_PROCESSING_OPTIONS](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_ALLOWED_PROCESSING_OPTIONS) config.
 - Add `imgproxy.source_image_origin` attribute to New Relic, DataDog, and OpenTelemetry traces.
 - Add `imgproxy.source_image_url` and `imgproxy.source_image_origin` attributes to `downloading_image` spans in New Relic, DataDog, and OpenTelemetry traces.
 - Add `imgproxy.processing_options` attribute to `processing_image` spans in New Relic, DataDog, and OpenTelemetry traces.

+ 3 - 0
config/config.go

@@ -48,6 +48,7 @@ var (
 	PngUnlimited                bool
 	SvgUnlimited                bool
 	MaxResultDimension          int
+	AllowedProcessiongOptions   []string
 	AllowSecurityOptions        bool
 
 	JpegProgressive       bool
@@ -254,6 +255,7 @@ func Reset() {
 	PngUnlimited = false
 	SvgUnlimited = false
 	MaxResultDimension = 0
+	AllowedProcessiongOptions = make([]string, 0)
 	AllowSecurityOptions = false
 
 	JpegProgressive = false
@@ -486,6 +488,7 @@ func Configure() error {
 	configurators.Bool(&SvgUnlimited, "IMGPROXY_SVG_UNLIMITED")
 
 	configurators.Int(&MaxResultDimension, "IMGPROXY_MAX_RESULT_DIMENSION")
+	configurators.StringSlice(&AllowedProcessiongOptions, "IMGPROXY_ALLOWED_PROCESSING_OPTIONS")
 
 	configurators.Bool(&AllowSecurityOptions, "IMGPROXY_ALLOW_SECURITY_OPTIONS")
 

+ 10 - 0
options/errors.go

@@ -35,6 +35,16 @@ func newUnknownOptionError(kind, opt string) error {
 	)
 }
 
+func newForbiddenOptionError(kind, opt string) error {
+	return ierrors.Wrap(
+		UnknownOptionError(fmt.Sprintf("Forbidden %s option %s", kind, opt)),
+		1,
+		ierrors.WithStatusCode(http.StatusNotFound),
+		ierrors.WithPublicMessage("Invalid URL"),
+		ierrors.WithShouldReport(false),
+	)
+}
+
 func (e UnknownOptionError) Error() string { return string(e) }
 
 func newOptionArgumentError(format string, args ...interface{}) error {

+ 1 - 1
options/presets.go

@@ -59,7 +59,7 @@ func parsePreset(presetStr string) error {
 func ValidatePresets() error {
 	for name, opts := range presets {
 		po := NewProcessingOptions()
-		if err := applyURLOptions(po, opts, name); err != nil {
+		if err := applyURLOptions(po, opts, true, name); err != nil {
 			return fmt.Errorf("Error in preset `%s`: %s", name, err)
 		}
 	}

+ 9 - 3
options/processing_options.go

@@ -691,7 +691,7 @@ func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...stri
 
 			po.UsedPresets = append(po.UsedPresets, preset)
 
-			if err := applyURLOptions(po, p, append(usedPresets, preset)...); err != nil {
+			if err := applyURLOptions(po, p, true, append(usedPresets, preset)...); err != nil {
 				return err
 			}
 		} else {
@@ -1081,8 +1081,14 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
 	return newUnknownOptionError("processing", name)
 }
 
-func applyURLOptions(po *ProcessingOptions, options urlOptions, usedPresets ...string) error {
+func applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, usedPresets ...string) error {
+	allowAll = allowAll || len(config.AllowedProcessiongOptions) == 0
+
 	for _, opt := range options {
+		if !allowAll && !slices.Contains(config.AllowedProcessiongOptions, opt.Name) {
+			return newForbiddenOptionError("processing", opt.Name)
+		}
+
 		if err := applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
 			return err
 		}
@@ -1154,7 +1160,7 @@ func parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions,
 
 	options, urlParts := parseURLOptions(parts)
 
-	if err = applyURLOptions(po, options); err != nil {
+	if err = applyURLOptions(po, options, false); err != nil {
 		return nil, "", err
 	}
 

+ 35 - 0
options/processing_options_test.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"net/url"
 	"regexp"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/suite"
@@ -644,6 +645,40 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
 	s.Require().Equal(originURL, imageURL)
 }
 
+func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
+	config.AllowedProcessiongOptions = []string{"w", "h", "pr"}
+
+	presets["test1"] = urlOptions{
+		urlOption{Name: "blur", Args: []string{"0.2"}},
+	}
+
+	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
+
+	testCases := []struct {
+		options       string
+		expectedError string
+	}{
+		{options: "w:100/h:200", expectedError: ""},
+		{options: "w:100/h:200/blur:10", expectedError: "Forbidden processing option blur"},
+		{options: "w:100/h:200/pr:test1", expectedError: ""},
+		{options: "w:100/h:200/pr:test1/blur:10", expectedError: "Forbidden processing option blur"},
+	}
+
+	for _, tc := range testCases {
+		s.Run(strings.ReplaceAll(tc.options, "/", "_"), func() {
+			path := fmt.Sprintf("/%s/%s.png", tc.options, base64.RawURLEncoding.EncodeToString([]byte(originURL)))
+			_, _, err := ParsePath(path, make(http.Header))
+
+			if len(tc.expectedError) > 0 {
+				s.Require().Error(err)
+				s.Require().Equal(tc.expectedError, err.Error())
+			} else {
+				s.Require().NoError(err)
+			}
+		})
+	}
+}
+
 func TestProcessingOptions(t *testing.T) {
 	suite.Run(t, new(ProcessingOptionsTestSuite))
 }