mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-10-11 04:32:29 +02:00
Support wildcards with * in AllowedSources (excluding slashes) (#677)
* Support wildcards with * in AllowedSources (excluding slashes) * Compile allowed sources patterns when parsing config * Fix linter errors Co-authored-by: Christopher Hlubek <christopher.hlubek@networkteam.com>
This commit is contained in:
committed by
GitHub
parent
de65113b97
commit
dbd19649a6
53
config.go
53
config.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -36,22 +37,6 @@ func strEnvConfig(s *string, name string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func strSliceEnvConfig(s *[]string, name string) {
|
|
||||||
if env := os.Getenv(name); len(env) > 0 {
|
|
||||||
parts := strings.Split(env, ",")
|
|
||||||
|
|
||||||
for i, p := range parts {
|
|
||||||
parts[i] = strings.TrimSpace(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
*s = parts
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*s = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolEnvConfig(b *bool, name string) {
|
func boolEnvConfig(b *bool, name string) {
|
||||||
if env, err := strconv.ParseBool(os.Getenv(name)); err == nil {
|
if env, err := strconv.ParseBool(os.Getenv(name)); err == nil {
|
||||||
*b = env
|
*b = env
|
||||||
@@ -197,6 +182,38 @@ func presetFileConfig(p presets, filepath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func patternsEnvConfig(s *[]*regexp.Regexp, name string) {
|
||||||
|
if env := os.Getenv(name); len(env) > 0 {
|
||||||
|
parts := strings.Split(env, ",")
|
||||||
|
result := make([]*regexp.Regexp, len(parts))
|
||||||
|
|
||||||
|
for i, p := range parts {
|
||||||
|
result[i] = regexpFromPattern(strings.TrimSpace(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = result
|
||||||
|
} else {
|
||||||
|
*s = []*regexp.Regexp{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func regexpFromPattern(pattern string) *regexp.Regexp {
|
||||||
|
var result strings.Builder
|
||||||
|
// Perform prefix matching
|
||||||
|
result.WriteString("^")
|
||||||
|
for i, part := range strings.Split(pattern, "*") {
|
||||||
|
// Add a regexp match all without slashes for each wildcard character
|
||||||
|
if i > 0 {
|
||||||
|
result.WriteString("[^/]*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote other parts of the pattern
|
||||||
|
result.WriteString(regexp.QuoteMeta(part))
|
||||||
|
}
|
||||||
|
// It is safe to use regexp.MustCompile since the expression is always valid
|
||||||
|
return regexp.MustCompile(result.String())
|
||||||
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
Network string
|
Network string
|
||||||
Bind string
|
Bind string
|
||||||
@@ -258,7 +275,7 @@ type config struct {
|
|||||||
IgnoreSslVerification bool
|
IgnoreSslVerification bool
|
||||||
DevelopmentErrorsMode bool
|
DevelopmentErrorsMode bool
|
||||||
|
|
||||||
AllowedSources []string
|
AllowedSources []*regexp.Regexp
|
||||||
LocalFileSystemRoot string
|
LocalFileSystemRoot string
|
||||||
S3Enabled bool
|
S3Enabled bool
|
||||||
S3Region string
|
S3Region string
|
||||||
@@ -384,7 +401,7 @@ func configure() error {
|
|||||||
}
|
}
|
||||||
intEnvConfig(&conf.MaxAnimationFrames, "IMGPROXY_MAX_ANIMATION_FRAMES")
|
intEnvConfig(&conf.MaxAnimationFrames, "IMGPROXY_MAX_ANIMATION_FRAMES")
|
||||||
|
|
||||||
strSliceEnvConfig(&conf.AllowedSources, "IMGPROXY_ALLOWED_SOURCES")
|
patternsEnvConfig(&conf.AllowedSources, "IMGPROXY_ALLOWED_SOURCES")
|
||||||
|
|
||||||
intEnvConfig(&conf.AvifSpeed, "IMGPROXY_AVIF_SPEED")
|
intEnvConfig(&conf.AvifSpeed, "IMGPROXY_AVIF_SPEED")
|
||||||
boolEnvConfig(&conf.JpegProgressive, "IMGPROXY_JPEG_PROGRESSIVE")
|
boolEnvConfig(&conf.JpegProgressive, "IMGPROXY_JPEG_PROGRESSIVE")
|
||||||
|
@@ -73,7 +73,7 @@ imgproxy does not send CORS headers by default. Specify allowed origin to enable
|
|||||||
|
|
||||||
You can limit allowed source URLs:
|
You can limit allowed source URLs:
|
||||||
|
|
||||||
* `IMGPROXY_ALLOWED_SOURCES`: whitelist of source image URLs prefixes divided by comma. When blank, imgproxy allows all source image URLs. Example: `s3://,https://example.com/,local://`. Default: blank.
|
* `IMGPROXY_ALLOWED_SOURCES`: whitelist of source image URLs prefixes divided by comma. Wildcards can be included with `*` to match all characters except `/`. When blank, imgproxy allows all source image URLs. Example: `s3://,https://*.example.com/,local://`. Default: blank.
|
||||||
|
|
||||||
**⚠️Warning:** Be careful when using this config to limit source URL hosts, and always add a trailing slash after the host. Bad: `http://example.com`, good: `http://example.com/`. If you don't add a trailing slash, `http://example.com@baddomain.com` will be an allowed URL but the request will be made to `baddomain.com`.
|
**⚠️Warning:** Be careful when using this config to limit source URL hosts, and always add a trailing slash after the host. Bad: `http://example.com`, good: `http://example.com/`. If you don't add a trailing slash, `http://example.com@baddomain.com` will be an allowed URL but the request will be made to `baddomain.com`.
|
||||||
|
|
||||||
|
@@ -993,8 +993,8 @@ func isAllowedSource(imageURL string) bool {
|
|||||||
if len(conf.AllowedSources) == 0 {
|
if len(conf.AllowedSources) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, val := range conf.AllowedSources {
|
for _, allowedSource := range conf.AllowedSources {
|
||||||
if strings.HasPrefix(imageURL, string(val)) {
|
if allowedSource.MatchString(imageURL) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -105,22 +106,62 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() {
|
|||||||
assert.Equal(s.T(), imageTypePNG, getProcessingOptions(ctx).Format)
|
assert.Equal(s.T(), imageTypePNG, getProcessingOptions(ctx).Format)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseURLAllowedSource() {
|
func (s *ProcessingOptionsTestSuite) TestParseURLAllowedSources() {
|
||||||
conf.AllowedSources = []string{"local://", "http://images.dev/"}
|
tt := []struct {
|
||||||
|
name string
|
||||||
|
allowedSources []string
|
||||||
|
requestPath string
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "match http URL without wildcard",
|
||||||
|
allowedSources: []string{"local://", "http://images.dev/"},
|
||||||
|
requestPath: "/unsafe/plain/http://images.dev/lorem/ipsum.jpg",
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match http URL with wildcard in hostname single level",
|
||||||
|
allowedSources: []string{"local://", "http://*.mycdn.dev/"},
|
||||||
|
requestPath: "/unsafe/plain/http://a-1.mycdn.dev/lorem/ipsum.jpg",
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match http URL with wildcard in hostname multiple levels",
|
||||||
|
allowedSources: []string{"local://", "http://*.mycdn.dev/"},
|
||||||
|
requestPath: "/unsafe/plain/http://a-1.b-2.mycdn.dev/lorem/ipsum.jpg",
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no match s3 URL with allowed local and http URLs",
|
||||||
|
allowedSources: []string{"local://", "http://images.dev/"},
|
||||||
|
requestPath: "/unsafe/plain/s3://images/lorem/ipsum.jpg",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no match http URL with wildcard in hostname including slash",
|
||||||
|
allowedSources: []string{"local://", "http://*.mycdn.dev/"},
|
||||||
|
requestPath: "/unsafe/plain/http://other.dev/.mycdn.dev/lorem/ipsum.jpg",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
req := s.getRequest("/unsafe/plain/http://images.dev/lorem/ipsum.jpg")
|
for _, tc := range tt {
|
||||||
_, err := parsePath(context.Background(), req)
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
|
exps := make([]*regexp.Regexp, len(tc.allowedSources))
|
||||||
|
for i, pattern := range tc.allowedSources {
|
||||||
|
exps[i] = regexpFromPattern(pattern)
|
||||||
|
}
|
||||||
|
conf.AllowedSources = exps
|
||||||
|
|
||||||
require.Nil(s.T(), err)
|
req := s.getRequest(tc.requestPath)
|
||||||
}
|
_, err := parsePath(context.Background(), req)
|
||||||
|
if tc.expectedError {
|
||||||
func (s *ProcessingOptionsTestSuite) TestParseURLNotAllowedSource() {
|
require.Error(t, err)
|
||||||
conf.AllowedSources = []string{"local://", "http://images.dev/"}
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
req := s.getRequest("/unsafe/plain/s3://images/lorem/ipsum.jpg")
|
}
|
||||||
_, err := parsePath(context.Background(), req)
|
})
|
||||||
|
}
|
||||||
require.Error(s.T(), err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathBasic() {
|
func (s *ProcessingOptionsTestSuite) TestParsePathBasic() {
|
||||||
|
Reference in New Issue
Block a user