Kaynağa Gözat

Security instance

Viktor Sokolov 3 hafta önce
ebeveyn
işleme
0b972b74f4

+ 8 - 1
config.go

@@ -6,6 +6,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/fetcher"
 	processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
 	streamhandler "github.com/imgproxy/imgproxy/v3/handlers/stream"
+	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/workers"
 )
@@ -24,6 +25,7 @@ type Config struct {
 	Fetcher        fetcher.Config
 	Handlers       HandlerConfigs
 	Server         server.Config
+	Security       security.Config
 }
 
 // NewDefaultConfig creates a new default configuration
@@ -37,7 +39,8 @@ func NewDefaultConfig() Config {
 			Processing: processinghandler.NewDefaultConfig(),
 			Stream:     streamhandler.NewDefaultConfig(),
 		},
-		Server: server.NewDefaultConfig(),
+		Server:   server.NewDefaultConfig(),
+		Security: security.NewDefaultConfig(),
 	}
 }
 
@@ -75,5 +78,9 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
 		return nil, err
 	}
 
+	if _, err := security.LoadConfigFromEnv(&c.Security); err != nil {
+		return nil, err
+	}
+
 	return c, nil
 }

+ 3 - 2
handlers/processing/handler.go

@@ -25,6 +25,7 @@ type HandlerContext interface {
 	FallbackImage() auximageprovider.Provider
 	WatermarkImage() auximageprovider.Provider
 	ImageDataFactory() *imagedata.Factory
+	Security() *security.Security
 }
 
 // Handler handles image processing requests
@@ -102,7 +103,7 @@ func (h *Handler) newRequest(
 	}
 
 	// verify the signature (if any)
-	if err = security.VerifySignature(signature, path); err != nil {
+	if err = h.Security().VerifySignature(signature, path); err != nil {
 		return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategorySecurity))
 	}
 
@@ -129,7 +130,7 @@ func (h *Handler) newRequest(
 	monitoring.SetMetadata(ctx, mm)
 
 	// verify that image URL came from the valid source
-	err = security.VerifySourceURL(imageURL)
+	err = h.Security().VerifySourceURL(imageURL)
 	if err != nil {
 		return "", nil, mm, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategorySecurity))
 	}

+ 12 - 0
imgproxy.go

@@ -14,6 +14,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/memory"
 	"github.com/imgproxy/imgproxy/v3/monitoring/prometheus"
+	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/workers"
 )
@@ -39,6 +40,7 @@ type Imgproxy struct {
 	fetcher          *fetcher.Fetcher
 	imageDataFactory *imagedata.Factory
 	handlers         ImgproxyHandlers
+	security         *security.Security
 	config           *Config
 }
 
@@ -66,6 +68,11 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
 		return nil, err
 	}
 
+	security, err := security.New(&config.Security)
+	if err != nil {
+		return nil, err
+	}
+
 	imgproxy := &Imgproxy{
 		workers:          workers,
 		fallbackImage:    fallbackImage,
@@ -73,6 +80,7 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
 		fetcher:          fetcher,
 		imageDataFactory: idf,
 		config:           config,
+		security:         security,
 	}
 
 	imgproxy.handlers.Health = healthhandler.New()
@@ -187,3 +195,7 @@ func (i *Imgproxy) WatermarkImage() auximageprovider.Provider {
 func (i *Imgproxy) ImageDataFactory() *imagedata.Factory {
 	return i.imageDataFactory
 }
+
+func (i *Imgproxy) Security() *security.Security {
+	return i.security
+}

+ 1 - 1
processing/processing.go

@@ -215,7 +215,7 @@ func checkImageSize(
 		return width, height, nil
 	}
 
-	err := security.CheckDimensions(width, height, frames, secops)
+	err := secops.CheckDimensions(width, height, frames)
 
 	return width, height, err
 }

+ 121 - 0
security/config.go

@@ -0,0 +1,121 @@
+package security
+
+import (
+	"fmt"
+	"regexp"
+
+	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/ensure"
+	log "github.com/sirupsen/logrus"
+)
+
+// OptionsConfig represents the configuration for processing limits and security options
+type OptionsConfig struct {
+	MaxSrcResolution            int // Maximum allowed source image resolution (width × height)
+	MaxSrcFileSize              int // Maximum allowed source file size in bytes (0 = unlimited)
+	MaxAnimationFrames          int // Maximum number of frames allowed in animated images
+	MaxAnimationFrameResolution int // Maximum resolution allowed for each frame in animated images (0 = unlimited)
+	MaxResultDimension          int // Maximum allowed dimension (width or height) for result images (0 = unlimited)
+}
+
+// NewDefaultOptionsConfig returns a new OptionsConfig instance with default values
+func NewDefaultOptionsConfig() OptionsConfig {
+	return OptionsConfig{
+		MaxSrcResolution:            50000000,
+		MaxSrcFileSize:              0,
+		MaxAnimationFrames:          1,
+		MaxAnimationFrameResolution: 0,
+		MaxResultDimension:          0,
+	}
+}
+
+// LoadOptionsConfigFromEnv loads OptionsConfig from global config variables
+func LoadOptionsConfigFromEnv(c *OptionsConfig) (*OptionsConfig, error) {
+	c.MaxSrcResolution = config.MaxSrcResolution
+	c.MaxSrcFileSize = config.MaxSrcFileSize
+	c.MaxAnimationFrames = config.MaxAnimationFrames
+	c.MaxAnimationFrameResolution = config.MaxAnimationFrameResolution
+	c.MaxResultDimension = config.MaxResultDimension
+
+	return c, nil
+}
+
+// Validate validates the OptionsConfig values
+func (c *OptionsConfig) Validate() error {
+	if c.MaxSrcResolution <= 0 {
+		return fmt.Errorf("max src resolution should be greater than 0, now - %d", c.MaxSrcResolution)
+	}
+
+	if c.MaxSrcFileSize < 0 {
+		return fmt.Errorf("max src file size should be greater than or equal to 0, now - %d", c.MaxSrcFileSize)
+	}
+
+	if c.MaxAnimationFrames <= 0 {
+		return fmt.Errorf("max animation frames should be greater than 0, now - %d", c.MaxAnimationFrames)
+	}
+
+	return nil
+}
+
+// Config is the package-local configuration
+type Config struct {
+	Options              OptionsConfig    // Processing limits and security options
+	AllowSecurityOptions bool             // Whether to allow security-related processing options in URLs
+	AllowedSources       []*regexp.Regexp // List of allowed source URL patterns (empty = allow all)
+	Keys                 [][]byte         // List of the HMAC keys
+	Salts                [][]byte         // List of the HMAC salts
+	SignatureSize        int              // Size of the HMAC signature in bytes
+	TrustedSignatures    []string         // List of trusted signature sources
+}
+
+// NewDefaultConfig returns a new Config instance with default values.
+func NewDefaultConfig() Config {
+	return Config{
+		Options:              NewDefaultOptionsConfig(),
+		AllowSecurityOptions: false,
+		SignatureSize:        32,
+	}
+}
+
+// LoadConfigFromEnv overrides configuration variables from environment
+func LoadConfigFromEnv(c *Config) (*Config, error) {
+	c = ensure.Ensure(c, NewDefaultConfig)
+
+	if _, err := LoadOptionsConfigFromEnv(&c.Options); err != nil {
+		return nil, err
+	}
+
+	c.AllowSecurityOptions = config.AllowSecurityOptions
+	c.AllowedSources = config.AllowedSources
+	c.Keys = config.Keys
+	c.Salts = config.Salts
+	c.SignatureSize = config.SignatureSize
+	c.TrustedSignatures = config.TrustedSignatures
+
+	return c, nil
+}
+
+// Validate validates the configuration
+func (c *Config) Validate() error {
+	if err := c.Options.Validate(); err != nil {
+		return err
+	}
+
+	if len(c.Keys) != len(c.Salts) {
+		return fmt.Errorf("number of keys and number of salts should be equal. Keys: %d, salts: %d", len(c.Keys), len(c.Salts))
+	}
+
+	if len(c.Keys) == 0 {
+		log.Warning("No keys defined, so signature checking is disabled")
+	}
+
+	if len(c.Salts) == 0 {
+		log.Warning("No salts defined, so signature checking is disabled")
+	}
+
+	if c.SignatureSize < 1 || c.SignatureSize > 32 {
+		return fmt.Errorf("signature size should be within 1 and 32, now - %d", c.SignatureSize)
+	}
+
+	return nil
+}

+ 0 - 17
security/image_size.go

@@ -1,17 +0,0 @@
-package security
-
-func CheckDimensions(width, height, frames int, opts Options) error {
-	frames = max(frames, 1)
-
-	if frames > 1 && opts.MaxAnimationFrameResolution > 0 {
-		if width*height > opts.MaxAnimationFrameResolution {
-			return newImageResolutionError("Source image frame resolution is too big")
-		}
-	} else {
-		if width*height*frames > opts.MaxSrcResolution {
-			return newImageResolutionError("Source image resolution is too big")
-		}
-	}
-
-	return nil
-}

+ 19 - 0
security/options.go

@@ -12,6 +12,8 @@ type Options struct {
 	MaxResultDimension          int
 }
 
+// NOTE: Remove this function in imgproxy v4
+// TODO: Replace this with security.NewOptions() when ProcessingOptions gets config
 func DefaultOptions() Options {
 	return Options{
 		MaxSrcResolution:            config.MaxSrcResolution,
@@ -29,3 +31,20 @@ func IsSecurityOptionsAllowed() error {
 
 	return newSecurityOptionsError()
 }
+
+// CheckDimensions checks if the given dimensions are within the allowed limits
+func (o *Options) CheckDimensions(width, height, frames int) error {
+	frames = max(frames, 1)
+
+	if frames > 1 && o.MaxAnimationFrameResolution > 0 {
+		if width*height > o.MaxAnimationFrameResolution {
+			return newImageResolutionError("Source image frame resolution is too big")
+		}
+	} else {
+		if width*height*frames > o.MaxSrcResolution {
+			return newImageResolutionError("Source image resolution is too big")
+		}
+	}
+
+	return nil
+}

+ 28 - 0
security/security.go

@@ -0,0 +1,28 @@
+package security
+
+// Security represents the security package instance
+type Security struct {
+	config *Config
+}
+
+// New creates a new Security instance
+func New(config *Config) (*Security, error) {
+	if err := config.Validate(); err != nil {
+		return nil, err
+	}
+
+	return &Security{
+		config: config,
+	}, nil
+}
+
+// NewOptions creates a new security.Options instance
+func (s *Security) NewOptions() Options {
+	return Options{
+		MaxSrcResolution:            s.config.Options.MaxSrcResolution,
+		MaxSrcFileSize:              s.config.Options.MaxSrcFileSize,
+		MaxAnimationFrames:          s.config.Options.MaxAnimationFrames,
+		MaxAnimationFrameResolution: s.config.Options.MaxAnimationFrameResolution,
+		MaxResultDimension:          s.config.Options.MaxResultDimension,
+	}
+}

+ 5 - 7
security/signature.go

@@ -5,16 +5,14 @@ import (
 	"crypto/sha256"
 	"encoding/base64"
 	"slices"
-
-	"github.com/imgproxy/imgproxy/v3/config"
 )
 
-func VerifySignature(signature, path string) error {
-	if len(config.Keys) == 0 || len(config.Salts) == 0 {
+func (s *Security) VerifySignature(signature, path string) error {
+	if len(s.config.Keys) == 0 || len(s.config.Salts) == 0 {
 		return nil
 	}
 
-	if slices.Contains(config.TrustedSignatures, signature) {
+	if slices.Contains(s.config.TrustedSignatures, signature) {
 		return nil
 	}
 
@@ -23,8 +21,8 @@ func VerifySignature(signature, path string) error {
 		return newSignatureError("Invalid signature encoding")
 	}
 
-	for i := 0; i < len(config.Keys); i++ {
-		if hmac.Equal(messageMAC, signatureFor(path, config.Keys[i], config.Salts[i], config.SignatureSize)) {
+	for i := 0; i < len(s.config.Keys); i++ {
+		if hmac.Equal(messageMAC, signatureFor(path, s.config.Keys[i], s.config.Salts[i], s.config.SignatureSize)) {
 			return nil
 		}
 	}

+ 36 - 18
security/signature_test.go

@@ -6,60 +6,78 @@ import (
 	"github.com/stretchr/testify/suite"
 
 	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/testutil"
 )
 
 type SignatureTestSuite struct {
-	suite.Suite
+	testutil.LazySuite
+
+	config   testutil.LazyObj[*Config]
+	security testutil.LazyObj[*Security]
+}
+
+func (s *SignatureTestSuite) SetupSuite() {
+	s.config, _ = testutil.NewLazySuiteObj(
+		s,
+		func() (*Config, error) {
+			c := NewDefaultConfig()
+			return &c, nil
+		},
+	)
+
+	s.security, _ = testutil.NewLazySuiteObj(
+		s,
+		func() (*Security, error) {
+			return New(s.config())
+		},
+	)
 }
 
 func (s *SignatureTestSuite) SetupTest() {
 	config.Reset()
 
-	config.Keys = [][]byte{[]byte("test-key")}
-	config.Salts = [][]byte{[]byte("test-salt")}
+	s.config().Keys = [][]byte{[]byte("test-key")}
+	s.config().Salts = [][]byte{[]byte("test-salt")}
 }
 
 func (s *SignatureTestSuite) TestVerifySignature() {
-	err := VerifySignature("oWaL7QoW5TsgbuiS9-5-DI8S3Ibbo1gdB2SteJh3a20", "asd")
+	err := s.security().VerifySignature("oWaL7QoW5TsgbuiS9-5-DI8S3Ibbo1gdB2SteJh3a20", "asd")
 	s.Require().NoError(err)
 }
 
 func (s *SignatureTestSuite) TestVerifySignatureTruncated() {
-	config.SignatureSize = 8
+	s.config().SignatureSize = 8
 
-	err := VerifySignature("oWaL7QoW5Ts", "asd")
+	err := s.security().VerifySignature("oWaL7QoW5Ts", "asd")
 	s.Require().NoError(err)
 }
 
 func (s *SignatureTestSuite) TestVerifySignatureInvalid() {
-	err := VerifySignature("oWaL7QoW5Ts", "asd")
+	err := s.security().VerifySignature("oWaL7QoW5Ts", "asd")
 	s.Require().Error(err)
 }
 
 func (s *SignatureTestSuite) TestVerifySignatureMultiplePairs() {
-	config.Keys = append(config.Keys, []byte("test-key2"))
-	config.Salts = append(config.Salts, []byte("test-salt2"))
+	s.config().Keys = append(s.config().Keys, []byte("test-key2"))
+	s.config().Salts = append(s.config().Salts, []byte("test-salt2"))
 
-	err := VerifySignature("jYz1UZ7j1BCdSzH3pZhaYf0iuz0vusoOTdqJsUT6WXI", "asd")
+	err := s.security().VerifySignature("jYz1UZ7j1BCdSzH3pZhaYf0iuz0vusoOTdqJsUT6WXI", "asd")
 	s.Require().NoError(err)
 
-	err = VerifySignature("oWaL7QoW5TsgbuiS9-5-DI8S3Ibbo1gdB2SteJh3a20", "asd")
+	err = s.security().VerifySignature("oWaL7QoW5TsgbuiS9-5-DI8S3Ibbo1gdB2SteJh3a20", "asd")
 	s.Require().NoError(err)
 
-	err = VerifySignature("dtLwhdnPPis", "asd")
+	err = s.security().VerifySignature("dtLwhdnPPis", "asd")
 	s.Require().Error(err)
 }
 
 func (s *SignatureTestSuite) TestVerifySignatureTrusted() {
-	config.TrustedSignatures = []string{"truested"}
-	defer func() {
-		config.TrustedSignatures = []string{}
-	}()
+	s.config().TrustedSignatures = []string{"truested"}
 
-	err := VerifySignature("truested", "asd")
+	err := s.security().VerifySignature("truested", "asd")
 	s.Require().NoError(err)
 
-	err = VerifySignature("untrusted", "asd")
+	err = s.security().VerifySignature("untrusted", "asd")
 	s.Require().Error(err)
 }
 

+ 5 - 7
security/source.go

@@ -1,15 +1,13 @@
 package security
 
-import (
-	"github.com/imgproxy/imgproxy/v3/config"
-)
-
-func VerifySourceURL(imageURL string) error {
-	if len(config.AllowedSources) == 0 {
+// VerifySourceURL checks if the given imageURL is allowed based on
+// the configured AllowedSources.
+func (s *Security) VerifySourceURL(imageURL string) error {
+	if len(s.config.AllowedSources) == 0 {
 		return nil
 	}
 
-	for _, allowedSource := range config.AllowedSources {
+	for _, allowedSource := range s.config.AllowedSources {
 		if allowedSource.MatchString(imageURL) {
 			return nil
 		}