You've already forked imgproxy
mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-11-27 22:48:53 +02:00
Prohibit connecting to loopback, link-local multicast, and link-local unicast IP addresses by default
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Add
|
||||||
|
- Add the `IMGPROXY_ALLOW_LOOPBACK_SOURCE_ADDRESSES`, `IMGPROXY_ALLOW_LINK_LOCAL_SOURCE_ADDRESSES`, and `IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES` configs.
|
||||||
|
|
||||||
|
### Change
|
||||||
|
- Connecting to loopback, link-local multicast, and link-local unicast IP addresses when requesting source images is prohibited by default.
|
||||||
|
|
||||||
## [3.14.0] - 2023-03-07
|
## [3.14.0] - 2023-03-07
|
||||||
## Add
|
## Add
|
||||||
|
|||||||
@@ -85,7 +85,10 @@ var (
|
|||||||
IgnoreSslVerification bool
|
IgnoreSslVerification bool
|
||||||
DevelopmentErrorsMode bool
|
DevelopmentErrorsMode bool
|
||||||
|
|
||||||
AllowedSources []*regexp.Regexp
|
AllowedSources []*regexp.Regexp
|
||||||
|
AllowLoopbackSourceAddresses bool
|
||||||
|
AllowLinkLocalSourceAddresses bool
|
||||||
|
AllowPrivateSourceAddresses bool
|
||||||
|
|
||||||
SanitizeSvg bool
|
SanitizeSvg bool
|
||||||
|
|
||||||
@@ -275,6 +278,9 @@ func Reset() {
|
|||||||
DevelopmentErrorsMode = false
|
DevelopmentErrorsMode = false
|
||||||
|
|
||||||
AllowedSources = make([]*regexp.Regexp, 0)
|
AllowedSources = make([]*regexp.Regexp, 0)
|
||||||
|
AllowLoopbackSourceAddresses = false
|
||||||
|
AllowLinkLocalSourceAddresses = false
|
||||||
|
AllowPrivateSourceAddresses = true
|
||||||
|
|
||||||
SanitizeSvg = true
|
SanitizeSvg = true
|
||||||
|
|
||||||
@@ -404,6 +410,9 @@ func Configure() error {
|
|||||||
configurators.Int(&MaxRedirects, "IMGPROXY_MAX_REDIRECTS")
|
configurators.Int(&MaxRedirects, "IMGPROXY_MAX_REDIRECTS")
|
||||||
|
|
||||||
configurators.Patterns(&AllowedSources, "IMGPROXY_ALLOWED_SOURCES")
|
configurators.Patterns(&AllowedSources, "IMGPROXY_ALLOWED_SOURCES")
|
||||||
|
configurators.Bool(&AllowLoopbackSourceAddresses, "IMGPROXY_ALLOW_LOOPBACK_SOURCE_ADDRESSES")
|
||||||
|
configurators.Bool(&AllowLinkLocalSourceAddresses, "IMGPROXY_ALLOW_LINK_LOCAL_SOURCE_ADDRESSES")
|
||||||
|
configurators.Bool(&AllowPrivateSourceAddresses, "IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES")
|
||||||
|
|
||||||
configurators.Bool(&SanitizeSvg, "IMGPROXY_SANITIZE_SVG")
|
configurators.Bool(&SanitizeSvg, "IMGPROXY_SANITIZE_SVG")
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,11 @@ You can limit allowed source URLs with the following variable:
|
|||||||
|
|
||||||
✅ Good: `http://example.com/`
|
✅ Good: `http://example.com/`
|
||||||
|
|
||||||
If the trailing slash is absent, `http://example.com@baddomain.com` would be a permissable URL, however, the request would be made to `baddomain.com`.
|
If the trailing slash is absent, `http://example.com@baddomain.com` would be a permissable URL, however, the request would be made to `baddomain.com`.
|
||||||
|
|
||||||
|
* `IMGPROXY_ALLOW_LOOPBACK_SOURCE_ADDRESSES`: when `true`, allows connecting to loopback IP addresses (`127.0.0.1`-`127.255.255.255` and IPv6 analogues) when requesting source images. Default: `false`
|
||||||
|
* `IMGPROXY_ALLOW_LINK_LOCAL_SOURCE_ADDRESSES`: when `true`, allows connecting to link-local multicast and unicast IP addresses (`224.0.0.1`-`224.0.0.255`, `169.254.0.1`-`169.254.255.255`, and IPv6 analogues) when requesting source images. Default: `false`
|
||||||
|
* `IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES`: when `true`, allows connecting to private IP addresses (`10.0.0.0 - 10.255.255.255`, `172.16.0.0 - 172.31.255.255`, `192.168.0.0 - 192.168.255.255`, and IPv6 analogues) when requesting source images. Default: `true`
|
||||||
|
|
||||||
* `IMGPROXY_SANITIZE_SVG`: when `true`, imgproxy will remove scripts from SVG images to prevent XSS attacks. Defaut: `true`
|
* `IMGPROXY_SANITIZE_SVG`: when `true`, imgproxy will remove scripts from SVG images to prevent XSS attacks. Defaut: `true`
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
@@ -63,6 +64,9 @@ func initDownloading() error {
|
|||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
DualStack: true,
|
DualStack: true,
|
||||||
|
Control: func(network, address string, c syscall.RawConn) error {
|
||||||
|
return security.VerifySourceNetwork(address)
|
||||||
|
},
|
||||||
}).DialContext,
|
}).DialContext,
|
||||||
MaxIdleConns: 100,
|
MaxIdleConns: 100,
|
||||||
MaxIdleConnsPerHost: config.Concurrency + 1,
|
MaxIdleConnsPerHost: config.Concurrency + 1,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
type httpError interface {
|
type httpError interface {
|
||||||
@@ -16,16 +17,25 @@ type httpError interface {
|
|||||||
func wrapError(err error) error {
|
func wrapError(err error) error {
|
||||||
isTimeout := false
|
isTimeout := false
|
||||||
|
|
||||||
if errors.Is(err, context.Canceled) {
|
switch {
|
||||||
|
case errors.Is(err, context.DeadlineExceeded):
|
||||||
|
isTimeout = true
|
||||||
|
case errors.Is(err, context.Canceled):
|
||||||
return ierrors.New(
|
return ierrors.New(
|
||||||
499,
|
499,
|
||||||
fmt.Sprintf("The image request is cancelled: %s", err),
|
fmt.Sprintf("The image request is cancelled: %s", err),
|
||||||
msgSourceImageIsUnreachable,
|
msgSourceImageIsUnreachable,
|
||||||
)
|
)
|
||||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
case errors.Is(err, security.ErrSourceAddressNotAllowed), errors.Is(err, security.ErrInvalidSourceAddress):
|
||||||
isTimeout = true
|
return ierrors.New(
|
||||||
} else if httpErr, ok := err.(httpError); ok {
|
404,
|
||||||
isTimeout = httpErr.Timeout()
|
err.Error(),
|
||||||
|
msgSourceImageIsUnreachable,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
if httpErr, ok := err.(httpError); ok {
|
||||||
|
isTimeout = httpErr.Timeout()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isTimeout {
|
if !isTimeout {
|
||||||
|
|||||||
@@ -233,13 +233,8 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
|||||||
po, imageURL, err := options.ParsePath(path, r.Header)
|
po, imageURL, err := options.ParsePath(path, r.Header)
|
||||||
checkErr(ctx, "path_parsing", err)
|
checkErr(ctx, "path_parsing", err)
|
||||||
|
|
||||||
if !security.VerifySourceURL(imageURL) {
|
err = security.VerifySourceURL(imageURL)
|
||||||
sendErrAndPanic(ctx, "security", ierrors.New(
|
checkErr(ctx, "security", err)
|
||||||
404,
|
|
||||||
fmt.Sprintf("Source URL is not allowed: %s", imageURL),
|
|
||||||
"Invalid source",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
if po.Raw {
|
if po.Raw {
|
||||||
streamOriginImage(ctx, reqID, r, rw, po, imageURL)
|
streamOriginImage(ctx, reqID, r, rw, po, imageURL)
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ func (s *ProcessingHandlerTestSuite) SetupSuite() {
|
|||||||
require.Nil(s.T(), err)
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
config.LocalFileSystemRoot = filepath.Join(wd, "/testdata")
|
config.LocalFileSystemRoot = filepath.Join(wd, "/testdata")
|
||||||
|
// Disable keep-alive to test connection restrictions
|
||||||
|
config.ClientKeepAliveTimeout = 0
|
||||||
|
|
||||||
err = initialize()
|
err = initialize()
|
||||||
require.Nil(s.T(), err)
|
require.Nil(s.T(), err)
|
||||||
@@ -58,6 +60,7 @@ func (s *ProcessingHandlerTestSuite) SetupTest() {
|
|||||||
// We don't need config.LocalFileSystemRoot anymore as it is used
|
// We don't need config.LocalFileSystemRoot anymore as it is used
|
||||||
// only during initialization
|
// only during initialization
|
||||||
config.Reset()
|
config.Reset()
|
||||||
|
config.AllowLoopbackSourceAddresses = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) send(path string, header ...http.Header) *httptest.ResponseRecorder {
|
func (s *ProcessingHandlerTestSuite) send(path string, header ...http.Header) *httptest.ResponseRecorder {
|
||||||
@@ -210,6 +213,28 @@ func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
|
||||||
|
data := s.readTestFile("test1.png")
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
rw.Write(data)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
var rw *httptest.ResponseRecorder
|
||||||
|
|
||||||
|
u := fmt.Sprintf("/unsafe/rs:fill:4:4/plain/%s/test1.png", server.URL)
|
||||||
|
fmt.Println(u)
|
||||||
|
|
||||||
|
rw = s.send(u)
|
||||||
|
require.Equal(s.T(), 200, rw.Result().StatusCode)
|
||||||
|
|
||||||
|
config.AllowLoopbackSourceAddresses = false
|
||||||
|
rw = s.send(u)
|
||||||
|
require.Equal(s.T(), 404, rw.Result().StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
|
func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
|
||||||
vips.DisableLoadSupport(imagetype.PNG)
|
vips.DisableLoadSupport(imagetype.PNG)
|
||||||
defer vips.ResetLoadSupport()
|
defer vips.ResetLoadSupport()
|
||||||
|
|||||||
@@ -1,17 +1,57 @@
|
|||||||
package security
|
package security
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func VerifySourceURL(imageURL string) bool {
|
var ErrSourceAddressNotAllowed = errors.New("source address is not allowed")
|
||||||
|
var ErrInvalidSourceAddress = errors.New("invalid source address")
|
||||||
|
|
||||||
|
func VerifySourceURL(imageURL string) error {
|
||||||
if len(config.AllowedSources) == 0 {
|
if len(config.AllowedSources) == 0 {
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, allowedSource := range config.AllowedSources {
|
for _, allowedSource := range config.AllowedSources {
|
||||||
if allowedSource.MatchString(imageURL) {
|
if allowedSource.MatchString(imageURL) {
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
return ierrors.New(
|
||||||
|
404,
|
||||||
|
fmt.Sprintf("Source URL is not allowed: %s", imageURL),
|
||||||
|
"Invalid source",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifySourceNetwork(addr string) error {
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
host = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
return ErrInvalidSourceAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.AllowLoopbackSourceAddresses && ip.IsLoopback() {
|
||||||
|
return ErrSourceAddressNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.AllowLinkLocalSourceAddresses && (ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()) {
|
||||||
|
return ErrSourceAddressNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.AllowPrivateSourceAddresses && ip.IsPrivate() {
|
||||||
|
return ErrSourceAddressNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user