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:
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.
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.
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.
<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.
header / {
# Prevent browsers from incorrectly detecting non-scripts as scripts
X-Content-Type-Options "nosniff"
}
Nothing crazy here and just implemented the suggested header.
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.
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.