Mozilla's Observatory

— Bryan Allred

Mozilla has recently publicly opened up a new tool Observatory at observatory.mozilla.org. The tool aims to help developers and security professionals be aware of potential issues and help resolve them for a more safe and secure Internet. This will be a dive in to targetting our own website at revolvingcow.com and then listing the issues and how we resolved or justified them. The actual results can be viewed here.

First, we should probably declare where we will be making the modifications. Our server is running on Caddy which hosts content directly but also acts as a proxy to other internally hosted services (i.e. Hugo, Gogs, etc.). For this example we wanted to focus on our main site which is ran using Hugo on an internal port which Caddy then proxies all requests to. The nice thing about this setup is:

  1. Hugo can be automatically updated via source control
  2. Caddy can host multiple services and subdomains
  3. Caddy can handle the SSL and TLS configurations automatically leveraging Let’s Encrypt and the ACME protocol
  4. We can explicitly allow paths to bypass the proxy to serve specific static resources

Below is a clean version of the configuration:

revolvingcow.com www.revolvingcow.com {
    gzip
    proxy / localhost:5097 {
        except /.well-known
    }
    root /var/www/revolvingcow.com
}

To summarize we went from an F to an A+ with an hour investment of time.

Content Security Policy

    header / {
        Content-Security-Policy "default-src 'none';
                                 font-src fonts.googleapis.com fonts.gstatic.com cdnjs.cloudflare.com;
                                 img-src 'self' *.stripe.com;
                                 object-src 'none';
                                 script-src 'self' cdnjs.cloudflare.com checkout.stripe.com;
                                 style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com fonts.googleapis.com;
                                 connect-src 'self' *.stripe.com;
                                 frame-src 'self' *.stripe.com"

    }
Directive Value Description
default-src ‘none’ If we missed a directive or a new one is added we want to fail hard first.
font-src fonts.googleapis.com
fonts.gstatic.com
cdnjs.cloudflare.com
Our fonts come from Google (which has two addresses) and a CDN.
img-src ‘self’
*.stripe.com
Images should come from our local site or Stripe.io.
object-src ‘none’ We hate embedding objects :)
script-src ‘self’
cdnjs.cloudflare.com
checkout.stripe.com
All our scripts should be local, from a CDN, or from Stripe.io for payments.
style-src ‘self’
‘unsafe-inline’
cdnjs.cloudflare.com
fonts.googleapis.com
Most of our styles will come from local, CDN or Google. However, some inline styles are applied and for a quick turn around we went ahead and approved this. In the future we will be moving all inline styles in to our stylesheet since it is the proper thing to do.
connect-src ‘self’
*.stripe.com
All XMLHttpRequest, WebSocket, or EventSource requests should be from ourselves or Stripe.io.
frame-src ‘self’
*.stripe.com
Inititially we thought we didn’t load any frames, but it appears Stripe.io’s Checkout API does.

This does still present errors within the console similar to

Refused to execute inline script because it violates the following Content Security Policy directive: “script-src ‘self’ cdnjs.cloudflare.com checkout.stripe.com”. Either the ‘unsafe-inline’ keyword, a hash (‘sha256-yUVnIIasrC7yNIGYh0wvG7kUNfyCWyaJTAY45Bdqztk=’), or a nonce (‘nonce-…’) is required to enable inline execution.

This was due to the Google Analytics script being in the header of our HTML. Moving this in to its own file resolved the issue.

HTTP Strict Transport Security

    header / {
        # Only connect to this site and subdomains via HTTPS for the next year and also include in the preload list
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    }

Due to there being subdomains we wanted them included as well, but pretty much this is verbatim to the recommendation.

Subresource Integrity

    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js" integrity="sha384-CgeP3wqr9h5YanePjYLENwCTSSEz42NJkbFpAFgHWQz7u3Zk8D00752ScNpXqGjS" crossorigin="anonymous"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.96.1/js/materialize.min.js" integrity="sha384-jTuHJ2QIy2SvtA4DSlQe6o/OA1yrU7l8JHdmP1PSSeVohsNTdO7fYmcZVie/Ev/l" crossorigin="anonymous"></script>

So this was a fun one and one that was actually on our TODO list. For external script references it is possible to include an additional sanity check on the contents to ensure it has not been tampered with. This is accomplished by the browser by comparing the checksum of the external resource to one predefined within the source. We used https://www.srihash.org/ to generate our hashes for the added benefit of also determining if the external resource is CORS compliant as well.

X-Content-Type-Options

    header / {
        # Prevent browsers from incorrectly detecting non-scripts as scripts
        X-Content-Type-Options "nosniff"
    }

Nothing crazy here and just implemented the suggested header.

X-Frame-Options

    header / {
        Content-Security-Policy "...; frame-ancestors 'none'"

        # Block site from being framed
        X-Frame-Options "DENY"
    }

We went ahead and covered both areas (Content-Site-Policy and X-Frame-Options) just to be safe. Right now there are no uses of IFRAMEs within the site, but if there were we could make the appropriate adjustments which are described in more detail in the associated link.

X-XSS-Protection header not implemented

    header / {
        # Block pages from loading when they detect reflected XSS attacks
        X-XSS-Protection "1; mode=block"
    }

Again just implemented the suggested fix and tested to make sure nothing was broken.