diff --git a/README.md b/README.md index 606f62e..e95b3e2 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,14 @@ Reload the [codercat URL][], and you should now get an error message. You can specify multiple hosts as a comma separated list, or prefix a host value with `*.` to allow all sub-domains as well. +### Content-Type whitelist ### + +You can limit what content types can be proxied by using the `contentTypes` +flag. By default, this is set to `image/*`, meaning that imageproxy will +process any image types. You can specify multiple content types as a comma +separated list, and suffix values with `*` to perform a wildcard match. Set the +flag to an empty string to proxy all requests, regardless of content type. + ### Signed Requests ### Instead of a host whitelist, you can require that requests be signed. This is diff --git a/cmd/imageproxy/main.go b/cmd/imageproxy/main.go index a872e0c..9de0b55 100644 --- a/cmd/imageproxy/main.go +++ b/cmd/imageproxy/main.go @@ -51,7 +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") +var contentTypes = flag.String("contentTypes", "image/*", "comma separated list of allowed content types") func init() { flag.Var(&cache, "cache", "location to cache images (see https://github.com/willnorris/imageproxy#cache)") diff --git a/imageproxy.go b/imageproxy.go index 0a7d75c..1b75499 100644 --- a/imageproxy.go +++ b/imageproxy.go @@ -169,12 +169,13 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) { } contentType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) - if !validContentType(p.ContentTypes, contentType) { - http.Error(w, "forbidden content-type", http.StatusForbidden) + if resp.ContentLength != 0 && !validContentType(p.ContentTypes, contentType) { + msg := fmt.Sprintf("forbidden content-type: %q", contentType) + log.Print(msg) + http.Error(w, msg, http.StatusForbidden) return } w.Header().Set("Content-Type", contentType) - w.Header().Set("X-Content-Type-Options", "nosniff") copyHeader(w.Header(), resp.Header, "Content-Length") @@ -225,25 +226,10 @@ 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. +// validContentType returns whether contentType matches one of the allowed patterns. 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 + return true } for _, pattern := range patterns { diff --git a/imageproxy_test.go b/imageproxy_test.go index e084aeb..199219c 100644 --- a/imageproxy_test.go +++ b/imageproxy_test.go @@ -304,7 +304,7 @@ func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) { case "/error": return nil, errors.New("http protocol error") case "/nocontent": - raw = "HTTP/1.1 204 No Content\nContent-Type: image/png\n\n" + raw = "HTTP/1.1 204 No Content\n\n" case "/etag": raw = "HTTP/1.1 200 OK\nEtag: \"tag\"\n\n" case "/png": @@ -326,7 +326,8 @@ func TestProxy_ServeHTTP(t *testing.T) { Client: &http.Client{ Transport: testTransport{}, }, - Whitelist: []string{"good.test"}, + Whitelist: []string{"good.test"}, + ContentTypes: []string{"image/*"}, } tests := []struct { @@ -413,29 +414,49 @@ 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) - } - } -} + tests := []struct { + patterns []string + contentType string + valid bool + }{ + // no patterns + {nil, "", true}, + {nil, "text/plain", true}, + {[]string{}, "", true}, + {[]string{}, "text/plain", true}, -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) + // empty pattern + {[]string{""}, "", true}, + {[]string{""}, "text/plain", false}, + + // exact match + {[]string{"text/plain"}, "", false}, + {[]string{"text/plain"}, "text", false}, + {[]string{"text/plain"}, "text/html", false}, + {[]string{"text/plain"}, "text/plain", true}, + {[]string{"text/plain"}, "text/plaintext", false}, + {[]string{"text/plain"}, "text/plain+foo", false}, + + // wildcard match + {[]string{"text/*"}, "", false}, + {[]string{"text/*"}, "text", false}, + {[]string{"text/*"}, "text/html", true}, + {[]string{"text/*"}, "text/plain", true}, + {[]string{"text/*"}, "image/jpeg", false}, + + {[]string{"image/svg*"}, "image/svg", true}, + {[]string{"image/svg*"}, "image/svg+html", true}, + + // complete wildcard does not match + {[]string{"*"}, "text/foobar", false}, + + // multiple patterns + {[]string{"text/*", "image/*"}, "image/jpeg", true}, + } + for _, tt := range tests { + got := validContentType(tt.patterns, tt.contentType) + if want := tt.valid; got != want { + t.Errorf("validContentType(%q, %q) returned %v, want %v", tt.patterns, tt.contentType, got, want) } } }