Security -> Checker

This commit is contained in:
Viktor Sokolov
2025-09-12 13:16:46 +02:00
parent 0b972b74f4
commit 59481026cf
14 changed files with 102 additions and 122 deletions

View File

@@ -25,7 +25,7 @@ type HandlerContext interface {
FallbackImage() auximageprovider.Provider FallbackImage() auximageprovider.Provider
WatermarkImage() auximageprovider.Provider WatermarkImage() auximageprovider.Provider
ImageDataFactory() *imagedata.Factory ImageDataFactory() *imagedata.Factory
Security() *security.Security Security() *security.Checker
} }
// Handler handles image processing requests // Handler handles image processing requests

View File

@@ -2,11 +2,26 @@ package imagedata
import ( import (
"fmt" "fmt"
"net/http"
"github.com/imgproxy/imgproxy/v3/fetcher" "github.com/imgproxy/imgproxy/v3/fetcher"
"github.com/imgproxy/imgproxy/v3/ierrors" "github.com/imgproxy/imgproxy/v3/ierrors"
) )
type FileSizeError struct{}
func newFileSizeError() error {
return ierrors.Wrap(
FileSizeError{},
1,
ierrors.WithStatusCode(http.StatusUnprocessableEntity),
ierrors.WithPublicMessage("Invalid source image"),
ierrors.WithShouldReport(false),
)
}
func (e FileSizeError) Error() string { return "Source image file is too big" }
func wrapDownloadError(err error, desc string) error { func wrapDownloadError(err error, desc string) error {
return ierrors.Wrap( return ierrors.Wrap(
fetcher.WrapError(err), 0, fetcher.WrapError(err), 0,

View File

@@ -11,7 +11,6 @@ import (
"github.com/imgproxy/imgproxy/v3/asyncbuffer" "github.com/imgproxy/imgproxy/v3/asyncbuffer"
"github.com/imgproxy/imgproxy/v3/fetcher" "github.com/imgproxy/imgproxy/v3/fetcher"
"github.com/imgproxy/imgproxy/v3/imagetype" "github.com/imgproxy/imgproxy/v3/imagetype"
"github.com/imgproxy/imgproxy/v3/security"
) )
// Factory represents ImageData factory // Factory represents ImageData factory
@@ -101,7 +100,7 @@ func (f *Factory) sendRequest(ctx context.Context, url string, opts DownloadOpti
return req, nil, h, err return req, nil, h, err
} }
res, err = security.LimitResponseSize(res, opts.MaxSrcFileSize) res, err = limitResponseSize(res, opts.MaxSrcFileSize)
if err != nil { if err != nil {
if res != nil { if res != nil {
res.Body.Close() res.Body.Close()

View File

@@ -1,4 +1,4 @@
package security package imagedata
import ( import (
"io" "io"
@@ -28,11 +28,11 @@ func (lr *hardLimitReadCloser) Close() error {
return lr.r.Close() return lr.r.Close()
} }
// LimitResponseSize limits the size of the response body to MaxSrcFileSize (if set). // limitResponseSize limits the size of the response body to MaxSrcFileSize (if set).
// First, it tries to use Content-Length header to check the limit. // First, it tries to use Content-Length header to check the limit.
// If Content-Length is not set, it limits the size of the response body by wrapping // If Content-Length is not set, it limits the size of the response body by wrapping
// body reader with hard limit reader. // body reader with hard limit reader.
func LimitResponseSize(r *http.Response, limit int) (*http.Response, error) { func limitResponseSize(r *http.Response, limit int) (*http.Response, error) {
if limit == 0 { if limit == 0 {
return r, nil return r, nil
} }

View File

@@ -40,7 +40,7 @@ type Imgproxy struct {
fetcher *fetcher.Fetcher fetcher *fetcher.Fetcher
imageDataFactory *imagedata.Factory imageDataFactory *imagedata.Factory
handlers ImgproxyHandlers handlers ImgproxyHandlers
security *security.Security security *security.Checker
config *Config config *Config
} }
@@ -196,6 +196,6 @@ func (i *Imgproxy) ImageDataFactory() *imagedata.Factory {
return i.imageDataFactory return i.imageDataFactory
} }
func (i *Imgproxy) Security() *security.Security { func (i *Imgproxy) Security() *security.Checker {
return i.security return i.security
} }

View File

@@ -2,6 +2,7 @@ package options
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"net/http" "net/http"
"slices" "slices"
"strconv" "strconv"
@@ -120,6 +121,31 @@ type ProcessingOptions struct {
} }
func NewProcessingOptions() *ProcessingOptions { func NewProcessingOptions() *ProcessingOptions {
// NOTE: This is temporary hack until ProcessingOptions does not have Factory
securityCfg, err := security.LoadConfigFromEnv(nil)
if err != nil {
fmt.Println(err)
}
// NOTE: This is a temporary workaround for logrus bug that deadlocks
// if log is used within another log (issue 1448)
if len(securityCfg.Salts) == 0 {
securityCfg.Salts = [][]byte{[]byte("logrusbugworkaround")}
}
if len(securityCfg.Keys) == 0 {
securityCfg.Keys = [][]byte{[]byte("logrusbugworkaround")}
}
// END OF WORKAROUND
security, err := security.New(securityCfg)
if err != nil {
fmt.Println(err)
}
securityOptions := security.NewOptions()
// NOTE: This is temporary hack until ProcessingOptions does not have Factory
po := ProcessingOptions{ po := ProcessingOptions{
ResizingType: ResizeFit, ResizingType: ResizeFit,
Width: 0, Width: 0,
@@ -151,7 +177,7 @@ func NewProcessingOptions() *ProcessingOptions {
SkipProcessingFormats: append([]imagetype.Type(nil), config.SkipProcessingFormats...), SkipProcessingFormats: append([]imagetype.Type(nil), config.SkipProcessingFormats...),
UsedPresets: make([]string, 0, len(config.Presets)), UsedPresets: make([]string, 0, len(config.Presets)),
SecurityOptions: security.DefaultOptions(), SecurityOptions: securityOptions,
// Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed // Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed
defaultQuality: config.Quality, defaultQuality: config.Quality,

22
security/checker.go Normal file
View File

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

View File

@@ -6,72 +6,31 @@ import (
"github.com/imgproxy/imgproxy/v3/config" "github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ensure" "github.com/imgproxy/imgproxy/v3/ensure"
log "github.com/sirupsen/logrus" 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 // Config is the package-local configuration
type Config struct { type Config struct {
Options OptionsConfig // Processing limits and security options
AllowSecurityOptions bool // Whether to allow security-related processing options in URLs AllowSecurityOptions bool // Whether to allow security-related processing options in URLs
AllowedSources []*regexp.Regexp // List of allowed source URL patterns (empty = allow all) AllowedSources []*regexp.Regexp // List of allowed source URL patterns (empty = allow all)
Keys [][]byte // List of the HMAC keys Keys [][]byte // List of the HMAC keys
Salts [][]byte // List of the HMAC salts Salts [][]byte // List of the HMAC salts
SignatureSize int // Size of the HMAC signature in bytes SignatureSize int // Size of the HMAC signature in bytes
TrustedSignatures []string // List of trusted signature sources TrustedSignatures []string // List of trusted signature sources
DefaultOptions Options // Default security options
} }
// NewDefaultConfig returns a new Config instance with default values. // NewDefaultConfig returns a new Config instance with default values.
func NewDefaultConfig() Config { func NewDefaultConfig() Config {
return Config{ return Config{
Options: NewDefaultOptionsConfig(), DefaultOptions: Options{
MaxSrcResolution: 50000000,
MaxSrcFileSize: 0,
MaxAnimationFrames: 1,
MaxAnimationFrameResolution: 0,
MaxResultDimension: 0,
},
AllowSecurityOptions: false, AllowSecurityOptions: false,
SignatureSize: 32, SignatureSize: 32,
} }
@@ -81,10 +40,6 @@ func NewDefaultConfig() Config {
func LoadConfigFromEnv(c *Config) (*Config, error) { func LoadConfigFromEnv(c *Config) (*Config, error) {
c = ensure.Ensure(c, NewDefaultConfig) c = ensure.Ensure(c, NewDefaultConfig)
if _, err := LoadOptionsConfigFromEnv(&c.Options); err != nil {
return nil, err
}
c.AllowSecurityOptions = config.AllowSecurityOptions c.AllowSecurityOptions = config.AllowSecurityOptions
c.AllowedSources = config.AllowedSources c.AllowedSources = config.AllowedSources
c.Keys = config.Keys c.Keys = config.Keys
@@ -92,13 +47,27 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
c.SignatureSize = config.SignatureSize c.SignatureSize = config.SignatureSize
c.TrustedSignatures = config.TrustedSignatures c.TrustedSignatures = config.TrustedSignatures
c.DefaultOptions.MaxSrcResolution = config.MaxSrcResolution
c.DefaultOptions.MaxSrcFileSize = config.MaxSrcFileSize
c.DefaultOptions.MaxAnimationFrames = config.MaxAnimationFrames
c.DefaultOptions.MaxAnimationFrameResolution = config.MaxAnimationFrameResolution
c.DefaultOptions.MaxResultDimension = config.MaxResultDimension
return c, nil return c, nil
} }
// Validate validates the configuration // Validate validates the configuration
func (c *Config) Validate() error { func (c *Config) Validate() error {
if err := c.Options.Validate(); err != nil { if c.DefaultOptions.MaxSrcResolution <= 0 {
return err return fmt.Errorf("max src resolution should be greater than 0, now - %d", c.DefaultOptions.MaxSrcResolution)
}
if c.DefaultOptions.MaxSrcFileSize < 0 {
return fmt.Errorf("max src file size should be greater than or equal to 0, now - %d", c.DefaultOptions.MaxSrcFileSize)
}
if c.DefaultOptions.MaxAnimationFrames <= 0 {
return fmt.Errorf("max animation frames should be greater than 0, now - %d", c.DefaultOptions.MaxAnimationFrames)
} }
if len(c.Keys) != len(c.Salts) { if len(c.Keys) != len(c.Salts) {

View File

@@ -9,7 +9,6 @@ import (
type ( type (
SignatureError string SignatureError string
FileSizeError struct{}
ImageResolutionError string ImageResolutionError string
SecurityOptionsError struct{} SecurityOptionsError struct{}
SourceURLError string SourceURLError string
@@ -27,18 +26,6 @@ func newSignatureError(msg string) error {
func (e SignatureError) Error() string { return string(e) } func (e SignatureError) Error() string { return string(e) }
func newFileSizeError() error {
return ierrors.Wrap(
FileSizeError{},
1,
ierrors.WithStatusCode(http.StatusUnprocessableEntity),
ierrors.WithPublicMessage("Invalid source image"),
ierrors.WithShouldReport(false),
)
}
func (e FileSizeError) Error() string { return "Source image file is too big" }
func newImageResolutionError(msg string) error { func newImageResolutionError(msg string) error {
return ierrors.Wrap( return ierrors.Wrap(
ImageResolutionError(msg), ImageResolutionError(msg),

View File

@@ -4,6 +4,7 @@ import (
"github.com/imgproxy/imgproxy/v3/config" "github.com/imgproxy/imgproxy/v3/config"
) )
// Security options (part of processing options)
type Options struct { type Options struct {
MaxSrcResolution int MaxSrcResolution int
MaxSrcFileSize int MaxSrcFileSize int
@@ -12,18 +13,7 @@ type Options struct {
MaxResultDimension int MaxResultDimension int
} }
// NOTE: Remove this function in imgproxy v4 // NOTE: This function is a part of processing option, we'll move it in the next PR
// TODO: Replace this with security.NewOptions() when ProcessingOptions gets config
func DefaultOptions() Options {
return Options{
MaxSrcResolution: config.MaxSrcResolution,
MaxSrcFileSize: config.MaxSrcFileSize,
MaxAnimationFrames: config.MaxAnimationFrames,
MaxAnimationFrameResolution: config.MaxAnimationFrameResolution,
MaxResultDimension: config.MaxResultDimension,
}
}
func IsSecurityOptionsAllowed() error { func IsSecurityOptionsAllowed() error {
if config.AllowSecurityOptions { if config.AllowSecurityOptions {
return nil return nil

View File

@@ -1,28 +0,0 @@
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,
}
}

View File

@@ -7,7 +7,7 @@ import (
"slices" "slices"
) )
func (s *Security) VerifySignature(signature, path string) error { func (s *Checker) VerifySignature(signature, path string) error {
if len(s.config.Keys) == 0 || len(s.config.Salts) == 0 { if len(s.config.Keys) == 0 || len(s.config.Salts) == 0 {
return nil return nil
} }

View File

@@ -13,7 +13,7 @@ type SignatureTestSuite struct {
testutil.LazySuite testutil.LazySuite
config testutil.LazyObj[*Config] config testutil.LazyObj[*Config]
security testutil.LazyObj[*Security] security testutil.LazyObj[*Checker]
} }
func (s *SignatureTestSuite) SetupSuite() { func (s *SignatureTestSuite) SetupSuite() {
@@ -27,7 +27,7 @@ func (s *SignatureTestSuite) SetupSuite() {
s.security, _ = testutil.NewLazySuiteObj( s.security, _ = testutil.NewLazySuiteObj(
s, s,
func() (*Security, error) { func() (*Checker, error) {
return New(s.config()) return New(s.config())
}, },
) )

View File

@@ -2,7 +2,7 @@ package security
// VerifySourceURL checks if the given imageURL is allowed based on // VerifySourceURL checks if the given imageURL is allowed based on
// the configured AllowedSources. // the configured AllowedSources.
func (s *Security) VerifySourceURL(imageURL string) error { func (s *Checker) VerifySourceURL(imageURL string) error {
if len(s.config.AllowedSources) == 0 { if len(s.config.AllowedSources) == 0 {
return nil return nil
} }