Click here to Skip to main content
14,420,655 members

Create a Content Security Policy (CSP) in Hugo

Rate this:
0.00 (No votes)
Please Sign up or sign in to vote.
0.00 (No votes)
21 Nov 2019CPOL
A Content Security Policy (CSP) helps prevent a variety of attacks on your site. This article describes how to implement one for a static website when you don't control the headers.

Create a Content Security Policy (CSP) in Hugo

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 must only 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Jeremy Likness
Instructor / Trainer Microsoft
United States United States
Note: articles posted here are independently written and do not represent endorsements nor reflect the views of my employer.

Jeremy Likness is a Cloud Developer Advocate for Azure at Microsoft. Jeremy has spent two decades building enterprise software with a focus on line of business web applications. Jeremy is the author of several highly acclaimed technical books including Designing Silverlight Business Applications and Programming the Windows Runtime by Example. He has given hundreds of technical presentations during his career as a professional developer. In his free time, Jeremy likes to CrossFit, hike, and maintain a 100% plant-based diet.

Jeremy's roles as business owner, technology executive and hands-on developer provided unique opportunities to directly impact the bottom line of multiple businesses by helping them grow and increase their organizational capacity while improving operational efficiency. He has worked with several initially small companies like Manhattan Associates and AirWatch before they grew large and experienced their transition from good to great while helping direct vision and strategy to embrace changing technology and markets. Jeremy is capable of quickly adapting to new paradigms and helps technology teams endure change by providing strong leadership, working with team members “in the trenches” and mentoring them in the soft skills that are key for engineers to bridge the gap between business and technology.

Comments and Discussions

 
-- There are no messages in this forum --
Technical Blog
Posted 21 Nov 2019

Stats

481 views
1 bookmarked