diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e498060..57daa8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,23 @@ - Removed `crop` resizing type, use [crop](./docs/generating_the_url.md#crop) processing option instead. - Dropped old libvips (<8.8) support. +## [2.17.0] - 2021-09-07 +### Added +- Wildcard support in `IMGPROXY_ALLOWED_SOURCES`. + +### Change +- If the source URL contains the `IMGPROXY_BASE_URL` prefix, it won't be added. + +### Fix +- (pro) Fix path prefix support in the `/info` handler. + +### Deprecated +- The [basic URL format](https://docs.imgproxy.net/generating_the_url_basic) is deprecated and can be removed in future versions. Use [advanced URL format](https://docs.imgproxy.net/generating_the_url_advanced) instead. + +## [2.16.7] - 2021-07-20 +### Change +- Reset DPI while stripping meta. + ## [2.16.6] - 2021-07-08 ### Fix - Fix performance regression in ICC profile handling. diff --git a/config/config.go b/config/config.go index bf1ef029..42e3ca81 100644 --- a/config/config.go +++ b/config/config.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "os" + "regexp" "runtime" log "github.com/sirupsen/logrus" @@ -72,7 +73,7 @@ var ( IgnoreSslVerification bool DevelopmentErrorsMode bool - AllowedSources []string + AllowedSources []*regexp.Regexp LocalFileSystemRoot string S3Enabled bool S3Region string @@ -200,7 +201,7 @@ func Reset() { IgnoreSslVerification = false DevelopmentErrorsMode = false - AllowedSources = make([]string, 0) + AllowedSources = make([]*regexp.Regexp, 0) LocalFileSystemRoot = "" S3Enabled = false S3Region = "" @@ -290,7 +291,7 @@ func Configure() error { configurators.Int(&MaxAnimationFrames, "IMGPROXY_MAX_ANIMATION_FRAMES") - configurators.StringSlice(&AllowedSources, "IMGPROXY_ALLOWED_SOURCES") + configurators.Patterns(&AllowedSources, "IMGPROXY_ALLOWED_SOURCES") configurators.Bool(&JpegProgressive, "IMGPROXY_JPEG_PROGRESSIVE") configurators.Bool(&PngInterlaced, "IMGPROXY_PNG_INTERLACED") diff --git a/config/configurators/configurators.go b/config/configurators/configurators.go index 7236b2fa..bc10780c 100644 --- a/config/configurators/configurators.go +++ b/config/configurators/configurators.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "os" + "regexp" "strconv" "strings" @@ -184,3 +185,35 @@ func HexFile(b *[][]byte, filepath string) error { return nil } + +func Patterns(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()) +} diff --git a/docs/configuration.md b/docs/configuration.md index 8e4817d6..3d55cea4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -73,7 +73,7 @@ imgproxy does not send CORS headers by default. Specify allowed origin to enable 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`. @@ -335,7 +335,7 @@ imgproxy can send logs to syslog, but this feature is disabled by default. To en ## Miscellaneous -* `IMGPROXY_BASE_URL`: base URL prefix that will be added to every requested image URL. For example, if the base URL is `http://example.com/images` and `/path/to/image.png` is requested, imgproxy will download the source image from `http://example.com/images/path/to/image.png`. Default: blank. +* `IMGPROXY_BASE_URL`: base URL prefix that will be added to every requested image URL. For example, if the base URL is `http://example.com/images` and `/path/to/image.png` is requested, imgproxy will download the source image from `http://example.com/images/path/to/image.png`. If the image URL already contains the prefix, it won't be added. Default: blank. * `IMGPROXY_USE_LINEAR_COLORSPACE`: when `true`, imgproxy will process images in linear colorspace. This will slow down processing. Note that images won't be fully processed in linear colorspace while shrink-on-load is enabled (see below). * `IMGPROXY_DISABLE_SHRINK_ON_LOAD`: when `true`, disables shrink-on-load for JPEG and WebP. Allows to process the whole image in linear colorspace but dramatically slows down resizing and increases memory usage when working with large images. * `IMGPROXY_STRIP_METADATA`: when `true`, imgproxy will strip all metadata (EXIF, IPTC, etc.) from JPEG and WebP output images. Default: `true`. diff --git a/imagedata/download.go b/imagedata/download.go index 3d59b43a..0fce5520 100644 --- a/imagedata/download.go +++ b/imagedata/download.go @@ -25,6 +25,9 @@ var ( "Cache-Control", "Expires", } + + // For tests + redirectAllRequestsTo string ) const msgSourceImageIsUnreachable = "Source image is unreachable" @@ -103,6 +106,11 @@ func requestImage(imageURL string) (*http.Response, error) { } func download(imageURL string) (*ImageData, error) { + // We use this for testing + if len(redirectAllRequestsTo) > 0 { + imageURL = redirectAllRequestsTo + } + res, err := requestImage(imageURL) if res != nil { defer res.Body.Close() @@ -140,3 +148,11 @@ func download(imageURL string) (*ImageData, error) { return imgdata, nil } + +func RedirectAllRequestsTo(u string) { + redirectAllRequestsTo = u +} + +func StopRedirectingRequests() { + redirectAllRequestsTo = "" +} diff --git a/landing.go b/landing.go index eb992b72..8ba57cd2 100644 --- a/landing.go +++ b/landing.go @@ -14,7 +14,7 @@ var landingTmpl = []byte(` `) func handleLanding(reqID string, rw http.ResponseWriter, r *http.Request) { - rw.Header().Set("Content-Tyle", "text/html") + rw.Header().Set("Content-Type", "text/html") rw.WriteHeader(200) rw.Write(landingTmpl) } diff --git a/options/url.go b/options/url.go index 71b220f2..3ead151c 100644 --- a/options/url.go +++ b/options/url.go @@ -12,6 +12,14 @@ import ( const urlTokenPlain = "plain" +func addBaseURL(u string) string { + if len(config.BaseURL) == 0 || strings.HasPrefix(u, config.BaseURL) { + return u + } + + return fmt.Sprintf("%s%s", config.BaseURL, u) +} + func decodeBase64URL(parts []string) (string, string, error) { var format string @@ -35,9 +43,7 @@ func decodeBase64URL(parts []string) (string, string, error) { return "", "", fmt.Errorf("Invalid url encoding: %s", encoded) } - fullURL := fmt.Sprintf("%s%s", config.BaseURL, string(imageURL)) - - return fullURL, format, nil + return addBaseURL(string(imageURL)), format, nil } func decodePlainURL(parts []string) (string, string, error) { @@ -63,9 +69,7 @@ func decodePlainURL(parts []string) (string, string, error) { return "", "", fmt.Errorf("Invalid url encoding: %s", encoded) } - fullURL := fmt.Sprintf("%s%s", config.BaseURL, unescaped) - - return fullURL, format, nil + return addBaseURL(unescaped), format, nil } func DecodeURL(parts []string) (string, string, error) { diff --git a/processing_handler_test.go b/processing_handler_test.go index 777058f6..a501e8d6 100644 --- a/processing_handler_test.go +++ b/processing_handler_test.go @@ -7,9 +7,12 @@ import ( "net/http/httptest" "os" "path/filepath" + "regexp" "testing" "github.com/imgproxy/imgproxy/v2/config" + "github.com/imgproxy/imgproxy/v2/config/configurators" + "github.com/imgproxy/imgproxy/v2/imagedata" "github.com/imgproxy/imgproxy/v2/imagemeta" "github.com/imgproxy/imgproxy/v2/imagetype" "github.com/imgproxy/imgproxy/v2/router" @@ -115,22 +118,66 @@ func (s *ProcessingHandlerTestSuite) TestSignatureValidationSuccess() { assert.Equal(s.T(), 200, res.StatusCode) } -func (s *ProcessingHandlerTestSuite) TestSourceValidationFailure() { - config.AllowedSources = []string{"https://"} +func (s *ProcessingHandlerTestSuite) TestSourceValidation() { + imagedata.RedirectAllRequestsTo("local:///test1.png") + defer imagedata.StopRedirectingRequests() - rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png") - res := rw.Result() + 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, + }, + } - assert.Equal(s.T(), 404, res.StatusCode) -} + for _, tc := range tt { + s.T().Run(tc.name, func(t *testing.T) { + exps := make([]*regexp.Regexp, len(tc.allowedSources)) + for i, pattern := range tc.allowedSources { + exps[i] = configurators.RegexpFromPattern(pattern) + } + config.AllowedSources = exps -func (s *ProcessingHandlerTestSuite) TestSourceValidationSuccess() { - config.AllowedSources = []string{"local:///"} + rw := s.send(tc.requestPath) + res := rw.Result() - rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png") - res := rw.Result() - - assert.Equal(s.T(), 200, res.StatusCode) + if tc.expectedError { + assert.Equal(s.T(), 404, res.StatusCode) + } else { + assert.Equal(s.T(), 200, res.StatusCode) + } + }) + } } func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() { diff --git a/security/source.go b/security/source.go index dfe6570d..c3d4fcd1 100644 --- a/security/source.go +++ b/security/source.go @@ -1,8 +1,6 @@ package security import ( - "strings" - "github.com/imgproxy/imgproxy/v2/config" ) @@ -10,8 +8,8 @@ func VerifySourceURL(imageURL string) bool { if len(config.AllowedSources) == 0 { return true } - for _, val := range config.AllowedSources { - if strings.HasPrefix(imageURL, string(val)) { + for _, allowedSource := range config.AllowedSources { + if allowedSource.MatchString(imageURL) { return true } } diff --git a/vips/vips.c b/vips/vips.c index 851a17b0..f0a8bb1f 100644 --- a/vips/vips.c +++ b/vips/vips.c @@ -523,7 +523,14 @@ vips_arrayjoin_go(VipsImage **in, VipsImage **out, int n) { int vips_strip(VipsImage *in, VipsImage **out) { - if (vips_copy(in, out, NULL)) return 1; + static double default_resolution = 72.0 / 25.4; + + if (vips_copy( + in, out, + "xres", default_resolution, + "yres", default_resolution, + NULL + )) return 1; gchar **fields = vips_image_get_fields(in);