Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restricted Content-Types #141

Closed
wants to merge 4 commits into from

Conversation

ccbrown
Copy link
Contributor

@ccbrown ccbrown commented Feb 9, 2018

For security purposes, we need to ensure that the proxy is only used to serve images. Serving non-image content opens up a lot of attack surface for phishing, XSS, SSRF, and other nasty tricks. Being labeled "imageproxy", I was surprised to find out that our proxies were happily serving any arbitrary HTML that was thrown at them.

I'm putting this PR in hoping that we can make the proxy more secure by default and bring it up to par with atmos/camo in this regard.

Our upcoming 4.7 release of Mattermost was all set to include built-in support for proxying user-posted images (via this or Camo), but we have a lot of very security-minded customers, so this put a slight wrinkle in our plans.

I've read over all of the related discussion I could find (namely this) and I think this addresses the big concerns except for one:

initially, I think the default should be to not check (see next point), so that this doesn't become a breaking change for anyone.

I strongly believe this should be enabled by default. It shouldn't be viewed as simply being a breaking change. It's a security patch. It's supposed to prevent people from doing things that they could do before.

The Changes

  • By default, the proxy will never return a Content-Type that isn't one of Camo's whitelisted image types. Their whitelist hasn't changed in 4 years and seems like a good, stable reference point.
  • If users are relying on the ability to proxy non-image types, they can specify them via the new -contentTypes flag. Shell pattern matching is used, so you can do things like -contentTypes image/*,video/mp4 if you'd like. Or you can just enable everything with wildcards to get today's behavior, but probably no one should do this.

This alters the behavior at the last moment, right before sending the response back to the client. This does not do sniffing. As stated in some of the other discussion, sniffing is pointless in the context of security. The only thing that matters is how browsers interpret the content. And browsers decide that based on the Content-Type and X-Content-Type-Options headers, which are now both set and strictly controlled.

@mikesimons
Copy link

👍 to this. If you have not set a host whitelist then imageproxy may happily proxy content from the network in which it is running too. For example http://my-site/images/200/http://169.254.169.254/latest/meta-data/ on AWS.

@julianzur
Copy link

Really looking forward for this PR to get merged. This will be a huge plus for security. Thanks, @ccbrown!

Copy link
Owner

@willnorris willnorris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the delay in reviewing, but thanks for this change... it definitely looks to be on the right track. A few suggested changes and questions below.

imageproxy.go Outdated
copyHeader(w.Header(), resp.Header, "Content-Length", "Content-Type")
if contentType := p.allowedContentType(resp.Header.Get("Content-Type")); contentType != "" {
w.Header().Set("Content-Type", contentType)
w.Header().Set("X-Content-Type-Options", "nosniff")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't whatwg/fetch#395 suggest that you shouldn't use nosniff with images?

And can this content type check simply be moved into the existing allowed() method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My current understanding is that it's fine (and more secure) as long you also specify the correct Content-Type. I should research and test that a bit more though.

I think the reason it's not in the existing allowed method is because it has to happen after we fetch the remote image.

imageproxy.go Outdated
@@ -211,6 +225,41 @@ func (p *Proxy) allowed(r *Request) error {
return fmt.Errorf("request does not contain an allowed host or valid signature: %v", r)
}

// allowedContentType returns an allowed content type string to use in responses or "" if the
// content type cannot be used.
func (p *Proxy) allowedContentType(contentType string) string {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why return a string? It doesn't look like the return value is really needed. Instead, perhaps rename this method to validContentType and return a bool like the other valid* methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there was a good reason for this, but I can't remember it. I'll refactor if it doesn't come to me.

imageproxy.go Outdated

if len(p.ContentTypes) == 0 {
switch mediaType {
case "image/bmp", "image/cgm", "image/g3fax", "image/gif", "image/ief", "image/jp2",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having to list out all of these images types seems really messy and error prone. You've already added support for image/* style wildcards below, so instead of treating an empty ContentTypes as special, just have the default flag value explicitly be image/*. That results in safe default behavior (just images), but still allows for the existing behavior (all types), by simply passing an empty value for the contentTypes flag.

Or is there some other image/* subtype that is deemed unsafe that you're specifically trying to avoid?

Copy link
Contributor Author

@ccbrown ccbrown Jun 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a security PR, I feel a lot better about using a more restrictive whitelist that's already been put to the test in other applications.

I can't say with much certainty that there are not dangerous image/* mimetypes. But perhaps more importantly, we can't say there will never be a dangerous image/* mimetype in the future (if there isn't already one).

So while there's nothing specifically that I'm aware of needing to avoid, I think it would be a security best practice to use a whitelist such as this one.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where did this whitelist come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -299,20 +299,20 @@ func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var raw string

switch req.URL.Path {
case "/ok":
case "/plain":
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the behavior I suggested above (namely that an empty ContentTypes value is not treated special), then this change should be reverted, as well as the corresponding StatusForbidden below.

@ccbrown
Copy link
Contributor Author

ccbrown commented Jun 22, 2018

Thanks for taking a look @willnorris. I'll try to re-familiarize myself with this PR and at least look into the nosniff thing / allowedContentType signature tomorrow.

@ccbrown
Copy link
Contributor Author

ccbrown commented Jun 22, 2018

@willnorris I made the requested allowedContentType -> validContentType refactor.

As for using "nosniff", the fact that Camo uses it by default combined with the fact that many very large and popular websites use "nosniff" for images has me convinced that there is not a compatibility issue with modern browsers. And I think there's a very real risk of some browsers sniffing proxied resources into non-image types if the header is omitted.

@hmhealey
Copy link
Contributor

Is there any update on the status of this PR? We've renewed our focus on adding a built-in image proxy to Mattermost, and this feature would be very important for that

@willnorris
Copy link
Owner

@ccbrown's changes merged in 39a4e18, with a few additional changes in 0370572. Thanks @ccbrown for your work on this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants