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
WatermarkImage() auximageprovider.Provider
ImageDataFactory() *imagedata.Factory
Security() *security.Security
Security() *security.Checker
}
// Handler handles image processing requests

View File

@@ -2,11 +2,26 @@ package imagedata
import (
"fmt"
"net/http"
"github.com/imgproxy/imgproxy/v3/fetcher"
"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 {
return ierrors.Wrap(
fetcher.WrapError(err), 0,

View File

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

View File

@@ -1,4 +1,4 @@
package security
package imagedata
import (
"io"
@@ -28,11 +28,11 @@ func (lr *hardLimitReadCloser) Close() error {
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.
// If Content-Length is not set, it limits the size of the response body by wrapping
// 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 {
return r, nil
}

View File

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

View File

@@ -2,6 +2,7 @@ package options
import (
"encoding/base64"
"fmt"
"net/http"
"slices"
"strconv"
@@ -120,6 +121,31 @@ type ProcessingOptions struct {
}
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{
ResizingType: ResizeFit,
Width: 0,
@@ -151,7 +177,7 @@ func NewProcessingOptions() *ProcessingOptions {
SkipProcessingFormats: append([]imagetype.Type(nil), config.SkipProcessingFormats...),
UsedPresets: make([]string, 0, len(config.Presets)),
SecurityOptions: security.DefaultOptions(),
SecurityOptions: securityOptions,
// Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed
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/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
DefaultOptions Options // Default security options
}
// NewDefaultConfig returns a new Config instance with default values.
func NewDefaultConfig() Config {
return Config{
Options: NewDefaultOptionsConfig(),
DefaultOptions: Options{
MaxSrcResolution: 50000000,
MaxSrcFileSize: 0,
MaxAnimationFrames: 1,
MaxAnimationFrameResolution: 0,
MaxResultDimension: 0,
},
AllowSecurityOptions: false,
SignatureSize: 32,
}
@@ -81,10 +40,6 @@ func NewDefaultConfig() Config {
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
@@ -92,13 +47,27 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
c.SignatureSize = config.SignatureSize
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
}
// Validate validates the configuration
func (c *Config) Validate() error {
if err := c.Options.Validate(); err != nil {
return err
if c.DefaultOptions.MaxSrcResolution <= 0 {
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) {

View File

@@ -9,7 +9,6 @@ import (
type (
SignatureError string
FileSizeError struct{}
ImageResolutionError string
SecurityOptionsError struct{}
SourceURLError string
@@ -27,18 +26,6 @@ func newSignatureError(msg string) error {
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 {
return ierrors.Wrap(
ImageResolutionError(msg),

View File

@@ -4,6 +4,7 @@ import (
"github.com/imgproxy/imgproxy/v3/config"
)
// Security options (part of processing options)
type Options struct {
MaxSrcResolution int
MaxSrcFileSize int
@@ -12,18 +13,7 @@ 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,
MaxSrcFileSize: config.MaxSrcFileSize,
MaxAnimationFrames: config.MaxAnimationFrames,
MaxAnimationFrameResolution: config.MaxAnimationFrameResolution,
MaxResultDimension: config.MaxResultDimension,
}
}
// NOTE: This function is a part of processing option, we'll move it in the next PR
func IsSecurityOptionsAllowed() error {
if config.AllowSecurityOptions {
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"
)
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 {
return nil
}

View File

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

View File

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