Jeremy Likness
Jeremy Likness
Empowering developers to be their best.
📅 Jul 12, 2019 🕘 5 min read 💬 981 words

Create a Content Security Policy (CSP) in Hugo

A configurable approach based on least privilege.

Part of the series: From Medium to Hugo

You are viewing a limited version of this blog. To enable experiences like comments, opt-in to our privacy and cookie policy.

A Content Security Policy (CSP) helps prevent attacks like Cross Site Scripting (XSS) and data-injection. A typical attack can occur when you include JavaScript from a third-party site. If the JavaScript from trusteddomain.com is somehow compromised, the script may be altered to load data from (or send data to) untrusteddomain.com. A CSP will prevent that by explicitly blocking actions from domains you don’t trust.

Below is an example of a violation that I captured from my console. I trust Google to serve ads, but don’t allow eval to run (this allows dynamic code from strings to be evaluated and executed, as opposed to static code that is included in files). Certain ads try to use this feature and are stopped cold by the CSP. If all ads tried to do this, I would simply remove them altogether or switch to another provider.

CSP violation

CSP violation

The typical way to implement a CSP is by serving an HTTP header named Content-Security-Policy. Most web servers can be configured to provide this, and some static hosting services like Netlify allow you to specify a special file that is parsed to include custom headers. If these options aren’t available, you can implement your CSP with a meta tag. This is the CSP policy for my blog as of this writing.

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests; block-all-mixed-content; default-src 'self'; child-src 'self'; font-src 'self' https://use.fontawesome.com https://fonts.gstatic.com https://public.slidesharecdn.com disqus.com disquscdn.com *.disqus.com *.disquscdn.com; form-action 'self' https://syndication.twitter.com/ https://platform.twitter.com disqus.com disquscdn.com *.disqus.com *.disquscdn.com; frame-src 'self' https://cse.google.com https://player.vimeo.com www.youtube.com www.youtube-nocookie.com https://platform.twitter.com https://syndication.twitter.com jsfiddle.net www.instagram.com https://kuula.co https://www.slideshare.net https://www.google.com https://googleads.g.doubleclick.net disqus.com disquscdn.com *.disqus.com *.disquscdn.com; img-src 'self' https://jeremylikness.visualstudio.com https://www.google-analytics.com https://stats.g.doubleclick.net https://pagead2.googlesyndication.com *.google.com *.gstatic.com *.amazon-adsystem.com https://www.googleapis.com https://images-na.ssl-images-amazon.com https://platform.twitter.com *.twimg.com https://syndication.twitter.com data: https://files.kuula.io disqus.com disquscdn.com *.disqus.com *.disquscdn.com; object-src 'none'; style-src 'self' 'unsafe-inline' https://use.fontawesome.com https://fonts.googleapis.com https://www.google.com https://platform.twitter.com *.twimg.com disqus.com disquscdn.com *.disqus.com *.disquscdn.com; script-src 'self' 'unsafe-inline' https://platform.twitter.com https://cdn.syndication.twimg.com *.amazon-adsystem.com https://www.google-analytics.com cse.google.com https://www.google.com https://pagead2.googlesyndication.com https://adservice.google.com https://www.googletagservices.com jsfiddle.net www.instagram.com disqus.com disquscdn.com *.disqus.com *.disquscdn.com;">

As you can see, there is a lot of content. I initially tried to maintain it by hand, but that quickly became unwieldy. So, I decided on a different approach: configuration. In my config.toml for the site I added a special section for my CSP.

[params.csp]
  childsrc = ["'self'"]
  fontsrc = ["'self'"]
  formaction = ["'self'"]
  framesrc = ["'self'"]
  imgsrc = ["'self'"]
  objectsrc = ["'none'"]
  stylesrc = ["'self'"]
  scriptsrc = ["'self'"]

This is what I started with and almost nothing worked because I rely on content front third-party sites (for example, I need access to Twitter if I want to embed tweets).

First, let’s break down the categories:

  • child-src configures behavior for web workers and nested scripts (i.e. in an iframe tag)
  • font-src configures where fonts can be loaded from
  • form-action restricts where form data can be submitted to
  • frame-src configures what domains can serve content to iframe tags
  • img-src configures image sources
  • object-src configures plug-ins
  • style-src configures stylesheets
  • script-src configures JavaScript behavior

The value none prohibits anything from happening. I won’t allow plug-ins anywhere on my site. The value self only allows resources to be served from the domain the CSP is hosted on. After turning on the CSP, you have a few options to monitor it. Exceptions will display in the console. You can also configure a Reporting Endpoint that instructs the browser to send information to an HTTPS endpoint. This is useful for gathering and tracking information from your deployed website.

I simply accessed various pages and corrected violations as they appeared. For example, to embed YouTube videos I need to allow frame-src access to the youtube.com or youtube-nocookie.com domains (the latter allows me to show videos without tracking your user data). I use Disqus for comments and that requires script, form, and other access. Eventually I built up a list of domains. Here’s the full list for JavaScript:

[params.csp]
  scriptsrc = ["'self'",
    "'unsafe-inline'",
    "https://platform.twitter.com",
    "https://cdn.syndication.twimg.com",
    "*.amazon-adsystem.com",
    "https://www.google-analytics.com",
    "cse.google.com",
    "https://www.google.com",
    "https://pagead2.googlesyndication.com",
    "https://adservice.google.com",
    "https://www.googletagservices.com",
    "jsfiddle.net",
    "www.instagram.com",
    "disqus.com",
    "disquscdn.com",
    "*.disqus.com",
    "*.disquscdn.com"]

The unsafe-inline allows using JavaScript embedded in <script> tags (as opposed to being loaded from external files). The fact that I don’t include unsafe-eval means no execution of JavaScript from dynamic strings is allowed. An alternative to unsafe-inline is to use a special sha256-hash approach. The violation will show a hash for the inline script, then you can add that hash to the CSP and as long as the inline script doesn’t change, it will be allowed.

To render the CSP from the configuration file, I created a partial template under /partials/shared named CSP.html. The code looks like this:

printf `<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests; block-all-mixed-content; default-src 'self'; ...` 

This is the first part. I upgrade any HTTP requests to HTTPS, so if a third-party vendor tries to fetch something without SSL it is automatically translated to a secure request. Although redundant, I’m also clear I don’t support mixed content (HTTP and HTTPS in the same page). By default, any item can be served from my domain. The individual policies are printed inline like this:

printf `... child-src %s; font-src %s ...`
    (delimit .Site.Params.csp.childsrc " ")
    (delimit .Site.Params.csp.fontsrc " ")
    | safeHTML

The %s is a placeholder. For each policy, I take the list from the configuration (.Site.Params.csp) and collapse it into a string using a space as the delimiter. This turns ["'self'", "'unsafe-inline'", "www.google.com"] into "'self' 'unsafe-inline' www.google.com". By default, the generated string is HTML encoded. The | safeHTML indicates that I trust the HTML and it doesn’t have to be encoded/escaped before rendering.

The last step was to include the partial template at the top of my header. In partials/_shared I added it to the top:

<head>
	<meta charset="utf-8">
	{{ partialCached "_shared/csp.html" . }}

I use partialCached to speed up rendering. It only must be evaluated once for the entire site, then every page is generated with it.

Now whenever I need to make a change, I simply tweak the configuration and I’m done. With a CSP in place, I feel a little more secure, and you should, too.

Regards,

Jeremy Likness

Do you have an idea or suggestion for a blog post? Submit it here!
comments powered by Disqus

Part of the series: From Medium to Hugo

  1. Migrate from Medium to Hugo
  2. More Hugo Migration Tips
  3. Dynamic Search in a Static Hugo Website
  4. Create a Content Security Policy (CSP) in Hugo
  5. Create an Article Preview in Hugo
  6. Implement a Progressive Web App (PWA) in your Static Website