Skip to content

Commit 1a9768a

Browse files
committedMar 22, 2023
Prohibit connecting to loopback, link-local multicast, and link-local unicast IP addresses by default
1 parent 24f4d43 commit 1a9768a

8 files changed

+110
-18
lines changed
 

‎CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22

33
## [Unreleased]
4+
### Add
5+
- Add the `IMGPROXY_ALLOW_LOOPBACK_SOURCE_ADDRESSES`, `IMGPROXY_ALLOW_LINK_LOCAL_SOURCE_ADDRESSES`, and `IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES` configs.
6+
7+
### Change
8+
- Connecting to loopback, link-local multicast, and link-local unicast IP addresses when requesting source images is prohibited by default.
49

510
## [3.14.0] - 2023-03-07
611
## Add

‎config/config.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ var (
8585
IgnoreSslVerification bool
8686
DevelopmentErrorsMode bool
8787

88-
AllowedSources []*regexp.Regexp
88+
AllowedSources []*regexp.Regexp
89+
AllowLoopbackSourceAddresses bool
90+
AllowLinkLocalSourceAddresses bool
91+
AllowPrivateSourceAddresses bool
8992

9093
SanitizeSvg bool
9194

@@ -275,6 +278,9 @@ func Reset() {
275278
DevelopmentErrorsMode = false
276279

277280
AllowedSources = make([]*regexp.Regexp, 0)
281+
AllowLoopbackSourceAddresses = false
282+
AllowLinkLocalSourceAddresses = false
283+
AllowPrivateSourceAddresses = true
278284

279285
SanitizeSvg = true
280286

@@ -404,6 +410,9 @@ func Configure() error {
404410
configurators.Int(&MaxRedirects, "IMGPROXY_MAX_REDIRECTS")
405411

406412
configurators.Patterns(&AllowedSources, "IMGPROXY_ALLOWED_SOURCES")
413+
configurators.Bool(&AllowLoopbackSourceAddresses, "IMGPROXY_ALLOW_LOOPBACK_SOURCE_ADDRESSES")
414+
configurators.Bool(&AllowLinkLocalSourceAddresses, "IMGPROXY_ALLOW_LINK_LOCAL_SOURCE_ADDRESSES")
415+
configurators.Bool(&AllowPrivateSourceAddresses, "IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES")
407416

408417
configurators.Bool(&SanitizeSvg, "IMGPROXY_SANITIZE_SVG")
409418

‎docs/configuration.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,11 @@ You can limit allowed source URLs with the following variable:
102102

103103
✅ Good: `http://example.com/`
104104

105-
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`.
105+
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`.
106+
107+
* `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`
108+
* `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`
109+
* `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`
106110

107111
* `IMGPROXY_SANITIZE_SVG`: when `true`, imgproxy will remove scripts from SVG images to prevent XSS attacks. Defaut: `true`
108112

‎imagedata/download.go

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net"
1010
"net/http"
1111
"net/http/cookiejar"
12+
"syscall"
1213
"time"
1314

1415
"github.com/imgproxy/imgproxy/v3/config"
@@ -63,6 +64,9 @@ func initDownloading() error {
6364
Timeout: 30 * time.Second,
6465
KeepAlive: 30 * time.Second,
6566
DualStack: true,
67+
Control: func(network, address string, c syscall.RawConn) error {
68+
return security.VerifySourceNetwork(address)
69+
},
6670
}).DialContext,
6771
MaxIdleConns: 100,
6872
MaxIdleConnsPerHost: config.Concurrency + 1,

‎imagedata/error.go

+15-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88

99
"github.com/imgproxy/imgproxy/v3/ierrors"
10+
"github.com/imgproxy/imgproxy/v3/security"
1011
)
1112

1213
type httpError interface {
@@ -16,16 +17,25 @@ type httpError interface {
1617
func wrapError(err error) error {
1718
isTimeout := false
1819

19-
if errors.Is(err, context.Canceled) {
20+
switch {
21+
case errors.Is(err, context.DeadlineExceeded):
22+
isTimeout = true
23+
case errors.Is(err, context.Canceled):
2024
return ierrors.New(
2125
499,
2226
fmt.Sprintf("The image request is cancelled: %s", err),
2327
msgSourceImageIsUnreachable,
2428
)
25-
} else if errors.Is(err, context.DeadlineExceeded) {
26-
isTimeout = true
27-
} else if httpErr, ok := err.(httpError); ok {
28-
isTimeout = httpErr.Timeout()
29+
case errors.Is(err, security.ErrSourceAddressNotAllowed), errors.Is(err, security.ErrInvalidSourceAddress):
30+
return ierrors.New(
31+
404,
32+
err.Error(),
33+
msgSourceImageIsUnreachable,
34+
)
35+
default:
36+
if httpErr, ok := err.(httpError); ok {
37+
isTimeout = httpErr.Timeout()
38+
}
2939
}
3040

3141
if !isTimeout {

‎processing_handler.go

+2-7
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,8 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
233233
po, imageURL, err := options.ParsePath(path, r.Header)
234234
checkErr(ctx, "path_parsing", err)
235235

236-
if !security.VerifySourceURL(imageURL) {
237-
sendErrAndPanic(ctx, "security", ierrors.New(
238-
404,
239-
fmt.Sprintf("Source URL is not allowed: %s", imageURL),
240-
"Invalid source",
241-
))
242-
}
236+
err = security.VerifySourceURL(imageURL)
237+
checkErr(ctx, "security", err)
243238

244239
if po.Raw {
245240
streamOriginImage(ctx, reqID, r, rw, po, imageURL)

‎processing_handler_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ func (s *ProcessingHandlerTestSuite) SetupSuite() {
4040
require.Nil(s.T(), err)
4141

4242
config.LocalFileSystemRoot = filepath.Join(wd, "/testdata")
43+
// Disable keep-alive to test connection restrictions
44+
config.ClientKeepAliveTimeout = 0
4345

4446
err = initialize()
4547
require.Nil(s.T(), err)
@@ -58,6 +60,7 @@ func (s *ProcessingHandlerTestSuite) SetupTest() {
5860
// We don't need config.LocalFileSystemRoot anymore as it is used
5961
// only during initialization
6062
config.Reset()
63+
config.AllowLoopbackSourceAddresses = true
6164
}
6265

6366
func (s *ProcessingHandlerTestSuite) send(path string, header ...http.Header) *httptest.ResponseRecorder {
@@ -210,6 +213,28 @@ func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
210213
}
211214
}
212215

216+
func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
217+
data := s.readTestFile("test1.png")
218+
219+
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
220+
rw.WriteHeader(200)
221+
rw.Write(data)
222+
}))
223+
defer server.Close()
224+
225+
var rw *httptest.ResponseRecorder
226+
227+
u := fmt.Sprintf("/unsafe/rs:fill:4:4/plain/%s/test1.png", server.URL)
228+
fmt.Println(u)
229+
230+
rw = s.send(u)
231+
require.Equal(s.T(), 200, rw.Result().StatusCode)
232+
233+
config.AllowLoopbackSourceAddresses = false
234+
rw = s.send(u)
235+
require.Equal(s.T(), 404, rw.Result().StatusCode)
236+
}
237+
213238
func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
214239
vips.DisableLoadSupport(imagetype.PNG)
215240
defer vips.ResetLoadSupport()

‎security/source.go

+44-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,57 @@
11
package security
22

33
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
48
"github.com/imgproxy/imgproxy/v3/config"
9+
"github.com/imgproxy/imgproxy/v3/ierrors"
510
)
611

7-
func VerifySourceURL(imageURL string) bool {
12+
var ErrSourceAddressNotAllowed = errors.New("source address is not allowed")
13+
var ErrInvalidSourceAddress = errors.New("invalid source address")
14+
15+
func VerifySourceURL(imageURL string) error {
816
if len(config.AllowedSources) == 0 {
9-
return true
17+
return nil
1018
}
19+
1120
for _, allowedSource := range config.AllowedSources {
1221
if allowedSource.MatchString(imageURL) {
13-
return true
22+
return nil
1423
}
1524
}
16-
return false
25+
26+
return ierrors.New(
27+
404,
28+
fmt.Sprintf("Source URL is not allowed: %s", imageURL),
29+
"Invalid source",
30+
)
31+
}
32+
33+
func VerifySourceNetwork(addr string) error {
34+
host, _, err := net.SplitHostPort(addr)
35+
if err != nil {
36+
host = addr
37+
}
38+
39+
ip := net.ParseIP(host)
40+
if ip == nil {
41+
return ErrInvalidSourceAddress
42+
}
43+
44+
if !config.AllowLoopbackSourceAddresses && ip.IsLoopback() {
45+
return ErrSourceAddressNotAllowed
46+
}
47+
48+
if !config.AllowLinkLocalSourceAddresses && (ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()) {
49+
return ErrSourceAddressNotAllowed
50+
}
51+
52+
if !config.AllowPrivateSourceAddresses && ip.IsPrivate() {
53+
return ErrSourceAddressNotAllowed
54+
}
55+
56+
return nil
1757
}

0 commit comments

Comments
 (0)
Please sign in to comment.