From 39a4e1813d91754d4a514d1032433c644315e9eb Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Fri, 9 Feb 2018 15:50:57 -0600 Subject: [PATCH] content-type checking --- cmd/imageproxy/main.go | 4 ++++ imageproxy.go | 46 +++++++++++++++++++++++++++++++++++++++++- imageproxy_test.go | 38 +++++++++++++++++++++++++++++----- 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/cmd/imageproxy/main.go b/cmd/imageproxy/main.go index 966d6b6..a872e0c 100644 --- a/cmd/imageproxy/main.go +++ b/cmd/imageproxy/main.go @@ -51,6 +51,7 @@ var scaleUp = flag.Bool("scaleUp", false, "allow images to scale beyond their or var timeout = flag.Duration("timeout", 0, "time limit for requests served by this proxy") var verbose = flag.Bool("verbose", false, "print verbose logging messages") var version = flag.Bool("version", false, "Deprecated: this flag does nothing") +var contentTypes = flag.String("contentTypes", "", "comma separated list of allowed content types") func init() { flag.Var(&cache, "cache", "location to cache images (see https://github.com/willnorris/imageproxy#cache)") @@ -66,6 +67,9 @@ func main() { if *referrers != "" { p.Referrers = strings.Split(*referrers, ",") } + if *contentTypes != "" { + p.ContentTypes = strings.Split(*contentTypes, ",") + } if *signatureKey != "" { key := []byte(*signatureKey) if strings.HasPrefix(*signatureKey, "@") { diff --git a/imageproxy.go b/imageproxy.go index a7fdc55..0a7d75c 100644 --- a/imageproxy.go +++ b/imageproxy.go @@ -26,8 +26,10 @@ import ( "io" "io/ioutil" "log" + "mime" "net/http" "net/url" + "path/filepath" "strings" "time" @@ -67,6 +69,10 @@ type Proxy struct { // If true, log additional debug messages Verbose bool + + // ContentTypes specifies a list of content types to allow. An empty list means only image types + // are allowed. + ContentTypes []string } // NewProxy constructs a new proxy. The provided http RoundTripper will be @@ -162,7 +168,15 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) { return } - copyHeader(w.Header(), resp.Header, "Content-Length", "Content-Type") + contentType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if !validContentType(p.ContentTypes, contentType) { + http.Error(w, "forbidden content-type", http.StatusForbidden) + return + } + w.Header().Set("Content-Type", contentType) + w.Header().Set("X-Content-Type-Options", "nosniff") + + copyHeader(w.Header(), resp.Header, "Content-Length") //Enable CORS for 3rd party applications w.Header().Set("Access-Control-Allow-Origin", "*") @@ -211,6 +225,36 @@ func (p *Proxy) allowed(r *Request) error { return fmt.Errorf("request does not contain an allowed host or valid signature: %v", r) } +// validContentType returns whether contentType matches one of the patterns. If no patterns are +// given, validContentType returns true if contentType matches one of the whitelisted image types. +func validContentType(patterns []string, contentType string) bool { + if len(patterns) == 0 { + switch contentType { + case "image/bmp", "image/cgm", "image/g3fax", "image/gif", "image/ief", "image/jp2", + "image/jpeg", "image/jpg", "image/pict", "image/png", "image/prs.btif", "image/svg+xml", + "image/tiff", "image/vnd.adobe.photoshop", "image/vnd.djvu", "image/vnd.dwg", + "image/vnd.dxf", "image/vnd.fastbidsheet", "image/vnd.fpx", "image/vnd.fst", + "image/vnd.fujixerox.edmics-mmr", "image/vnd.fujixerox.edmics-rlc", + "image/vnd.microsoft.icon", "image/vnd.ms-modi", "image/vnd.net-fpx", "image/vnd.wap.wbmp", + "image/vnd.xiff", "image/webp", "image/x-cmu-raster", "image/x-cmx", "image/x-icon", + "image/x-macpaint", "image/x-pcx", "image/x-pict", "image/x-portable-anymap", + "image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap", + "image/x-quicktime", "image/x-rgb", "image/x-xbitmap", "image/x-xpixmap", + "image/x-xwindowdump": + return true + } + return false + } + + for _, pattern := range patterns { + if ok, err := filepath.Match(pattern, contentType); ok && err == nil { + return true + } + } + + return false +} + // validHost returns whether the host in u matches one of hosts. func validHost(hosts []string, u *url.URL) bool { for _, host := range hosts { diff --git a/imageproxy_test.go b/imageproxy_test.go index aa5878a..e084aeb 100644 --- a/imageproxy_test.go +++ b/imageproxy_test.go @@ -299,12 +299,12 @@ func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) { var raw string switch req.URL.Path { - case "/ok": + case "/plain": raw = "HTTP/1.1 200 OK\n\n" case "/error": return nil, errors.New("http protocol error") case "/nocontent": - raw = "HTTP/1.1 204 No Content\n\n" + raw = "HTTP/1.1 204 No Content\nContent-Type: image/png\n\n" case "/etag": raw = "HTTP/1.1 200 OK\nEtag: \"tag\"\n\n" case "/png": @@ -312,7 +312,7 @@ func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) { img := new(bytes.Buffer) png.Encode(img, m) - raw = fmt.Sprintf("HTTP/1.1 200 OK\nContent-Length: %d\n\n%s", len(img.Bytes()), img.Bytes()) + raw = fmt.Sprintf("HTTP/1.1 200 OK\nContent-Length: %d\nContent-Type: image/png\n\n%s", len(img.Bytes()), img.Bytes()) default: raw = "HTTP/1.1 404 Not Found\n\n" } @@ -338,8 +338,8 @@ func TestProxy_ServeHTTP(t *testing.T) { {"/http://bad.test/", http.StatusForbidden}, // Disallowed host {"/http://good.test/error", http.StatusInternalServerError}, // HTTP protocol error {"/http://good.test/nocontent", http.StatusNoContent}, // non-OK response - - {"/100/http://good.test/ok", http.StatusOK}, + {"/100/http://good.test/png", http.StatusOK}, + {"/100/http://good.test/plain", http.StatusForbidden}, // non-image response } for _, tt := range tests { @@ -411,3 +411,31 @@ func TestTransformingTransport(t *testing.T) { } } } + +func TestValidContentType(t *testing.T) { + for contentType, expected := range map[string]bool{ + "": false, + "image/png": true, + "text/html": false, + } { + actual := validContentType(nil, contentType) + if actual != expected { + t.Errorf("got %v, expected %v for content type: %v", actual, expected, contentType) + } + } +} + +func TestValidContentType_Patterns(t *testing.T) { + for contentType, expected := range map[string]bool{ + "": false, + "image/png": false, + "foo/asdf": true, + "bar/baz": true, + "bar/bazz": false, + } { + actual := validContentType([]string{"foo/*", "bar/baz"}, contentType) + if actual != expected { + t.Errorf("got %v, expected %v for content type: %v", actual, expected, contentType) + } + } +}