News & Announcements

How I learned to stop worrying and love the Content Security Policy

News & Announcements

How I learned to stop worrying and love the Content Security Policy

💡 This blog post shows a dynamic approach of applying a Content Security Policy for the script-src directive. Other directives like connect-src would not benefit from this dynamic approach.

The importance of a Content Security Policy

The web is open by default: any website can fetch scripts, stylesheets, images, fonts, and more as a resource from any domain. However, that leaves the door open for malicious actors to execute scripts on your site and attack your visitors. Website owners who value security of their websites, as well as their customers who use them, are encouraged to increase the security posture of the frontend of their website(s) by implementing a Content Security Policy.

What is a Content Security Policy (CSP)?

A Content Security Policy (CSP) is “a mechanism by which web developers can control the resources which a particular page can fetch or execute”. It instructs the browser to restrict network requests to a set of trusted domains specified by the website. It is an effective tool in the security tool belt that helps prevent resources — scripts, stylesheets, images, etc — from loading or executing without the website owner’s consent.

When we refer to a CSP, we are talking about a website’s Content-Security-Policy response header. The header’s value is a string of text, a semicolon-separated list of directives (like script-src, image-src, connect-src, etc) and their source lists, a list of domains (or content hashes) separated by spaces. This response header controls which external resources are allowed to be accessed by the site.

A CSP should be considered by any organization concerned about client-side attacks, compliance, exfiltration of customer data, and website defacement.

Setting up a CSP is difficult to get right

The problem with CSP is that it is notoriously fragile as well as tedious to maintain. Here are some common pitfalls you may encounter when specifying an allowlist of domains or content hashes that your website’s frontend is allowed to talk to:

  • The CSP may block legitimate resources from loading or executing. If you forget to include a domain on this list, and your code legitimately attempts to fetch from this domain, then that request will be blocked.
  • Inline snippets will need their content hashes recomputed if they change. If you employ the script-src directive without 'unsafe-inline', then inline scripts won’t execute unless you also include a SHA of its content in the CSP as well. You’ll need to either manually hardcode this hash (and adjust as needed whenever the script changes) or write a build step that computes and injects this hash for you.
  • Traditionally, CSP response headers are static, not dynamic, when served from a CDN. This means you’d need to maintain an allowlist of every single domain referenced by your site, which can easily balloon up to tens or hundreds of domains. But don’t worry - we have a better solution. Skip down to Simplifying CSP with dynamically-generated nonces for a spoiler on how we can implement a dynamic nonce.

In an effort to ease the operational risk of making CSP changes, a common strategy is to first deploy a Content-Security-Policy-Report-Only (CSP-RO) response header. This essentially sets up a dry-run of the CSP, reporting any violations (resources that the browser would block due to the domain not being on the allowlist) to the URI specified in the CSP’s report-uri directive. With a CSP-RO in place, you can test your CSP candidate against real production traffic. However, this strategy isn’t foolproof:

  • Only domains that users access during this test period will be reported. Dusty corners of your app or site won’t be reported unless traffic is sent to them. Resources may point to different top-level domains (TLDs) based on the region the traffic is coming from (for example, .de domains may be accessed from Germany, whereas traffic from France might reach out to .fr domains), so if not all regions are active, then there will likely be holes in the reporting.
  • The test period is ongoing and it does not end. Typically, a CSP-RO is deployed alongside a CSP — acting kind of like a beta — where developers can test changes against production traffic to see if any violations are reported. If no violations are reported after some time, then the new rules are promoted to the CSP. The ongoing nature of CSP-RO is exhausting from a maintenance perspective.

Consider a marketing team that owns the Segment or Google Tag Manager implementation of a site. They want to add a script so that data starts flowing to this new destination. Unless this new destination’s domain is already included in the CSP, it won’t start working until developers make a code change to add it. This creates a bottleneck of marketers waiting on developers, as the code change will require human approval in a pull request.

As you can imagine, this allowlist would grow to become rather long. Take adding Google Analytics to the connect-src directive, for example: Google itself recommends that “each Google top-level domain (TLD) must be specified individually, since CSP syntax does not allow the use of wildcards on the right side of the hostname.” This number of Google domains is incredibly long (187 at time of writing!), and is what they recommend just to support a single service. Every external resource that the website reaches out to must be captured and documented, otherwise they will not load — it is strict by design. As a result, developers tend to be afraid to remove domains from this list, so it grows to be an append-only list of rules (much like a global stylesheet!). On top of that, this long allowlist is included in every response when served as a static header, inflating total request size and egress.

Many companies try to deploy a CSP, but never make it past the CSP-RO stage — including us! We had tried to enforce a CSP on the Netlify platform, but it never graduated from Report-Only, because 1) the workflow to approve new domains to the allowlist was too cumbersome, and 2) we weren’t confident that we were capturing all allowed domains in our proposed allowlist.

Subresource integrity solves a different problem

You may have heard of subresource integrity (SRI). Script tags include the integrity attribute, whose value must match the hashed value of the resource’s content, otherwise the browser will refuse to execute it. The hash for subresource integrity is static/deterministic; that is to say, the value does not change unless the content changes.

But implementing SRI is only helpful if you are hotloading scripts from a third-party. You want to be sure that the content you are downloading — such as a versioned library — does not change (either maliciously or accidentally). You also must calculate the sha ahead of time, and hardcode that value into your HTML, for it to become effective.

What SRI does not do is prevent cross-site scripting attacks (XSS). If you have user-generated content on your site, or collect user input from forms or query parameters, then your frontend developers must go through extra lengths to ensure that a maliciously-crafted input wouldn’t allow for scripts to be injected on the page. Modern frameworks like React have guardrails in place to prevent this, but there are some use cases (notably rendering markdown into HTML) where unsanitized markup can find a way to the client.

Simplifying CSP with dynamically-generated nonces

The crux of the difficulty behind implementing a CSP successfully is that manually maintaining an allowlist is tedious. What if instead of maintaining an allowlist, we could guarantee integrity dynamically?

That’s exactly what a CSP nonce does: on every request, a randomly-generated cryptographically-secure string gets included in the CSP’s script-src directive. If the random string from the CSP header does not equal the value in a <script> tag’s nonce attribute, then the browser will refuse to execute that script. Scripts that have a matching nonce attribute are considered to be trusted, and when the strict-dynamic keyword is specified, then any script(s) created from the trusted script will also be trusted and allowed to execute.

Using Netlify Edge Functions for dynamic CSP

Traditionally, CDNs aren’t able to dynamically generate response headers. The headers — like the content of the files themselves — are static, unless your site uses server-side rendering (SSR).

However, we have Netlify Edge Functions in our arsenal, which act like a middleware layer for the CDN. An edge function can intercept requests that match certain criteria (in this case, HTML documents), add a response header, and transform <script> tags to include a nonce attribute that is unique to every request.

By providing the script-src directive with a dynamically-generated nonce at request-time, attackers won’t be able to inject their own client-side scripts. Even data exfiltration — usually protected by the connect-src directive — becomes much more difficult.

How to implement dynamically-generated nonces on your Netlify sites

We’ve developed an integration using the Netlify SDK that will configure and deploy this edge function for you. Simply head to the integrations page of your site or team, and enable the Content Security Policy nonce integration:

Configure options on the resulting page:

Your next deploy will contain the CSP nonce. If your site already has a CSP in place, this integration will merge the nonce with your existing directives.

🧩 If you prefer configuration as code, you can also enable this functionality by installing the @netlify/plugin-csp-nonce build plugin.

Testing your dynamic CSP

You can test in a Deploy Preview before going live by clicking the Test button on the integration configuration page. And because all deploys on Netlify are atomic and immutable, you can instantly rollback if anything goes awry. Inspect your response headers, and you should see a CSP with a value like this:

script-src 'nonce-6RIGit1eDC3GY0BRqEifv/dQ3OyKCZ8w' 'strict-dynamic' 'unsafe-inline' 'unsafe-eval' 'self' https: http:; report-uri /.netlify/functions/__csp-violations

Here, the 'unsafe-inline', 'self', https:, and http: sources are supplied for backwards-compatibility purposes, and are ignored by browsers that support the strict-dynamic source list keyword. Modern browsers support strict-dynamic (CSP Level 3 in Chrome 52+, Edge 79+, Firefox 52+, and Safari 15.4+).

By default, the integration will use the Content-Security-Policy-Report-Only response header for your CSP. This means that violations will be logged, but not blocked, giving you insight into how production traffic behaves with the policy without breaking functionality. Violations will be reported to and logged by a Netlify Function also deployed by the integration — you can monitor the violation reports by checking the logs of the __csp-violations function.

To graduate from a Content-Security-Policy-Report-Only header to a Content-Security-Policy header, remove the 'unsafe-eval' source list keyword (present by default for easier adoption), or configure a custom report-uri, just change configuration values on the integration configuration page.

To gradually control the rollout of the nonce in your CSP while you monitor violation reports, you can set the CSP_NONCE_DISTRIBUTION environment variable to a value between 0 and 1. This value controls the percentage of traffic that would receive a transformed response containing the CSP nonce.

The edge function that transforms your site’s HTML to include the CSP nonce runs after snippet injection, so any user-defined snippets, as well as scripts supporting Real User Metrics (RUM) and the Netlify Drawer, will include the nonce attribute and won’t be blocked.

We’re so confident that this solution will help you implement a Content Security Policy for your site, that we’re putting our money where our mouth is: We currently use this edge function ourselves on 100% of requests to the Netlify platform. Inspect the response headers of the Netlify UI to see for yourself! We’ve been living with this in production for months now, and haven’t seen delays in response time or other ill effects. If Netlify Edge Functions start having a degraded or unavailable service, the edge function will by bypassed, meaning that the request chain will continue (albeit without the CSP nonce in place) rather than crashing altogether.

The dynamic nonce gets added to both external and inline scripts.

💡 Don’t forget to test your updated CSP against common browser extensions that your users use. We’ve tested and confirmed that the Google Lighthouse, Google Translate, and 1Password browser extensions all work when the integration is enabled.

Enhanced web security made simple with the Netlify Dynamic CSP integration

We’re excited for you to try out the CSP integration on your site. Let us know what you think in the forums! If you or your team encounters any issues, please let us know by filing an issue on the GitHub repo.

If you’re not a Netlify Enterprise customer, reach out to our team for a custom demo.

Keep reading

Recent posts

eBook cover

Better, faster, future-proof: Three keys to shipping stand-out digital experiences

Get my free copy