0
Fork 0
mirror of https://github.com/willnorris/imageproxy.git synced 2024-12-16 21:56:43 -05:00

content-type checking

This commit is contained in:
Christopher Brown 2018-02-09 15:50:57 -06:00 committed by Will Norris
parent 74c16f575e
commit 39a4e1813d
3 changed files with 82 additions and 6 deletions

View file

@ -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, "@") {

View file

@ -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 {

View file

@ -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)
}
}
}