Web security: With content security policy against cross-site scripting, part 1

Page 2: CSP Script Hashes

Contents

Many applications use inline script blocks to load legitimate JavaScript code. The following snippet shows a simplified example:

// index.html 
<button id="button">Say Hello!</button> 
<script> 
  document.addEventListener("DOMContentLoaded", () => { 
    document.getElementById("button") 
      .addEventListener("click", () => { alert("Clicked")}); 
  }) 
</script> 

The previous CSP policy would block this JavaScript code because it is a fundamentally blocked inline script block. To execute it without security restrictions, CSP Level 2 can include the hash of a script block in the policy.

For this purpose, a hash value (e.g. SHA-256) of the regular inline script is calculated, which serves as a digital signature for the script. To release the script, the hash value is added to the CSP policy of the website.

The following CSP policy allows the code block to be executed:

<button id="button">Say Hello!</button> 
<script nonce=2104tk118mk> 
document.addEventListener("DOMContentLoaded", () => { 
  document.getElementById("button") 
      .addEventListener("click", () => { alert("Clicked")}); 
}) 
</script> 

When the browser loads a page, it calculates the hash values of the inline scripts and compares them with the values specified in the CSP. The browser only executes the script if the hash values match. Otherwise, it blocks the execution as it considers the code to be unauthorized.

The security of this mechanism is based on the properties of the hash function: only this JavaScript code generates the hash defined in the policy. Even a minimal change, such as adding a space, would completely change the hash of the code.

To use script hashes to determine which specific inline scripts may be executed on the website, the hashes of all scripts must be generated and added to the CSP header. There are numerous online tools such as the Online Script and Style Hasher from ReportURI. Normally, automated build tools generate the hashes.

In a Chromium-based browser, the expected hash can be found in the error messages of the developer console:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'".
Either the 'unsafe-inline' keyword, a hash ('sha256-xd4UE9RSFsHsg3ZcbtC21uvtZ++ffUUSxChTCyIHZpA='), or a nonce ('nonce-...') is required to enable inline execution.

This means the following: The browser has blocked the inline script because it violates the CSP directive script-src 'self'. To execute the code, either the keyword 'unsafe-inline', the hash sha256-xd4UE9RSFsHsg3ZcbtC21uvtZ++ffUUSxChTCyIHZpA= or a nonce (nonce-...) is required.

If you are sure that the hash in the message belongs to a legitimate code block, you can add it to the CSP directive.

Even if CSP hashes are well suited for checking small inline scripts, it can be very time-consuming to recalculate the hash again and again after a change to the script. In addition, many inline scripts with individual hash values lead to a bloated CSP. There is therefore another variant to allow the execution of inline scripts.

This is where the nonces come into play, which appear in the HTML of a legitimate application as follows, for example:

<button id="button">Say Hello!</button> 
<script nonce=2104tk118mk> 
document.addEventListener("DOMContentLoaded", () => { 
  document.getElementById("button") 
      .addEventListener("click", () => { alert("Clicked")}); 
}) 
</script> 

When the server generates the website, it creates a random nonce value. This value is inserted both in the CSP header of the web page and integrated as an attribute in the corresponding <script> tags:

Content-Security-Policy: script-src 'nonce=2104tk118mk'

When the browser loads a web page and encounters a script with a nonce attribute, it compares the nonce value of the script with the value specified in the CSP. The browser only executes the script if the nonce values match. This ensures that only those scripts are executed that the server has specifically approved for this request.

It is important that nonces are regenerated each time a page is called up. Nonces should come from a cryptographically secure random source and must never be reused. Otherwise, an attacker could predict the nonce and incorporate it into malicious code blocks.

In contrast to hashes, nonces can also be used with script tags that are intended to load an external JavaScript file into the browser via the src attribute:

<script src="https://my-site/index.js" nonce="nonce=2104tk118mk"></script>

In this case, a nonce in the script tag is sufficient to tell the browser that this code is legitimate.

In the error message shown above for the missing hash, the browser suggests adding 'unsafe-inline' as a value to the script-src directive instead of using CSP hashes or CSP nonces. However, this procedure is considered unsafe.

The value 'unsafe-inline' refers specifically to the execution of inline scripts and styles. Anyone who uses 'unsafe-inline' in the CSP guideline for scripts (script-src) explicitly allows the embedding and execution of inline scripts on the website. While this allows legitimate application scripts to run, it also opens the door for malicious code to be injected. Ultimately, 'unsafe-inline' largely overrides the protective measures of CSP.

Nevertheless, 'unsafe-inline' is common in many CSP policies. This is because many older and even some modern web applications are heavily dependent on inline JavaScript. Moving to an architecture that is compatible with CSP without explicit permission to run inline scripts often requires extensive code changes to offload scripts to external files. 'unsafe-inline' allows these applications to implement CSP without having to make extensive customizations immediately.

However, legacy browsers such as Internet Explorer have not implemented CSP Level 2, which means that neither CSP nonces nor CSP hashes are evaluated. This leads to unwanted behavior if the CSP rules only rely on these mechanisms.

As a result, many CSP policies include both nonces and 'unsafe-inline', although the latter undermines the security benefit of CSP and should only be included if you need to support legacy browsers. Fortunately, modern browsers ignore a set 'unsafe-inline' as soon as a CSP hash or CSP nonce value is set.

The Content Security Policy offers additional protection against XSS vulnerabilities. However, the first line of defense must be provided by the web application itself. Although the original CSP protects against inline scripts, it also blocks legitimate scripts. CSP Level 2 introduces hashes and nonces to distinguish the application's scripts from injected code.

The mechanisms described in the first part of the two-part series already provide a good basis for protection against cross-site scripting. However, some questions remain unanswered:

What happens if a script that has been loaded externally from a content delivery network, for example, wants to reload further scripts? Are these included in the CSP? Can CSP not be used if the HTTP response headers cannot be set manually? How can nonce-based protection be used if the server and frontend are deployed separately and delivered from different servers?

These questions will be addressed in the second part of this two-part article.

(vbr)