EdgeWorkers CSP: Cutting Latency, Not Security

EdgeWorkers CSP: Cutting Latency, Not Security

Content Security Policy (CSP) is a security feature implemented in web browsers to protect websites and web applications from various types of attacks, such as cross-site scripting (XSS) and data injection attacks. CSP provides protection by controlling and limiting the sources from which various types of content, such as scripts, stylesheets, and images, can be loaded and executed on a web page.

Here’s how CSP works and how it ties into the HTML source:

  1. Defining a CSP Header:
    • A CSP is typically defined server-side by setting a special HTTP header called Content-Security-Policy in the response sent by the web server when a user requests a web page. This header specifies the rules that the browser should follow when loading and executing content on the page. The CSP header can be defined in various ways depending on your server technology. For example, in Node, you can set it like this: res.set(“Content-Security-Policy: directive1 value1; directive2 value2; …”);
  2. Directives and Values:
    * The CSP header consists of directives and their corresponding values. These directives define the rules for various types of resources. For example, the script-src directive specifies the allowed sources for JavaScript files, and the style-src directive specifies the allowed sources for stylesheets.
    * Example CSP header: Content-Security-Policy: default-src ‘self’; script-src ‘self’ https://static.example.com; style-src ‘self’ ‘unsafe-inline’; In this example, default-src ‘self’ specifies that by default, all content should be loaded from the same origin as the page itself. script-src allows scripts to be loaded from the same origin and from the specified external source (https://static.example.com in this case). style-src allows stylesheets to be loaded from the same origin and also allows inline styles.
  3. Tying into HTML:
    • Once the CSP header is set on the server, the browser enforces these rules when loading resources on the web page.
    • In the HTML source, developers can add elements like and to reference external resources. The browser will then check the CSP header to ensure that these resources comply with the defined rules. If a resource is not allowed by the CSP, the browser will block its execution.
  4. Unique on Every Request:
    • CSP headers are typically static and consistent for all requests to a particular web page. They are not unique on every request.
    • However, CSP can be used in combination with nonce values or hashes to allow for dynamic inline scripts and styles while maintaining security. Nonces and hashes ensure that even if the source of a script or style is not explicitly listed in the CSP, it can still be executed if it matches the nonce or hash value specified in the CSP header. These nonce values or hashes can change on each request, making them unique, and they need to be generated and managed on the server-side. Or do they?!

Proposed Solution with EdgeWorkers:

Implementing the dynamic CSP nonce at the Edge with EdgeWorkers can provide significant advantages. Nonce generation and insertion server-side can be difficult to do and can impact performance. Let’s delve into some reasons why this approach is advantageous:

  1. Reduced Latency: Generating nonces with EdgeWorkers offers a solution that eliminates the need for high-latency calls to the origin server. This is achieved by dynamically generating and embedding nonces into cached content, a process that traditionally posed challenges due to the requirement that the CSP be unique for each request. With EdgeWorkers, we can overcome this challenge by generating and embedding nonces into cached content at the Edge for every request. This serverless and distributed approach ensures that CSPs are served from edge locations in close geographical proximity to users. Furthermore, EdgeWorkers abstracts the underlying infrastructure, providing a scalable environment for CSP processing that operates independently of the primary application servers.
  2. Distributed Security: By implementing CSP nonce generation at the edge, you establish an additional layer of security distinct from the application code. In the event of application vulnerabilities, the CSP EdgeWorker can help mitigate risks before requests even reach your end users, providing an added layer of defense.
  3. Ease of Maintenance: Managing the dynamic nonce for the CSP at the edge with a serverless approach simplifies maintenance tasks. You can centrally update CSP policies at the edge without modifying the application code itself. This reduces deployment complexities and the likelihood of introducing bugs. Additionally, using EdgeWorkers provides a central point of control for multiple independent applications developed by different teams governed by a single consistent EdgeWorker applied across all applications.

While this article primarily discusses the generation and insertion of CSP nonces into your CSP headers and HTML, it’s equally feasible to extend the advantages described here to manage the complete CSP header using EdgeWorkers and EdgeKV. This approach would also simplify the implementation of customized CSP policies tailored to specific pages.

The implementation can be broken down into three parts. The full code sample can be found here: https://github.com/akamai/edgeworkers-examples/tree/master/edgecompute/examples/security/csp.

Let’s first set the baseline of what our origin server is sending that we’ll write our EdgeWorker to work on. The CSP Header:

And the HTML snippet:

  1. Generate the Nonce: A “nonce” is a cryptographic value that changes with each request. It is a critical component in preventing certain types of attacks, such as cross-site scripting (XSS). The generated nonce should be a cryptographically secure random number which we support through the EdgeWorker crypto module and getRandomValues() function as documented here. We append a prefix (“nonce-”) to the value which is considered a best practice. This nonce will be used to allow only trusted scripts to execute on the page.
    • Generating a Random Number: The crypto.getRandomValues() method is called with a Uint32Array of length 1. This method generates a cryptographically secure random number (32 bits) and stores it in the array. Cryptographically secure randomness is important for generating nonces to ensure they cannot be easily predicted or manipulated by attackers.
      let array = new Uint32Array(1);
      crypto.getRandomValues(array);
    • Base64 Encoding: The string representation of the random number is encoded using Base64 encoding via the btoa() function exported by the encoding module in EdgeWorkers as documented here. Base64 encoding is a common way to convert binary data into a string of ASCII characters, making it suitable for use in HTTP Headers and HTML. Here it ensures that the nonce value contains characters that are safe to include in both HTML attributes and HTTP headers.
      let encodedData = btoa(stringToEncode);
  2. Append the Nonce to the CSP Header: The Content Security Policy (CSP) header defines what sources of content are considered safe for loading and execution. In this step, the code retrieves the original CSP header from the origin response. It then parses the CSP policy using a custom function (parsePolicy) and isolates the directive related to script sources (script-src). By replacing the original script sources with the generated nonce, the code ensures that only scripts with this specific nonce will be executed. This effectively prevents unauthorized scripts from running.
    • Getting the origin response: The code uses the httpRequest() EdgeWorker module to asynchronously fetch content from the origin.
      let htmlResponse = await httpRequest(“/”);
    • Extracting the Origin CSP Header: The response received from the origin contains headers, including the Content-Security-Policy (CSP) header. The getHeader() function retrieves an array of values associated with the ‘Content-Security-Policy’ header.
      let originCSPHeader = htmlResponse.getHeader(‘Content-Security-Policy’)[0];
    • Parsing the CSP Policy: The parsePolicy() function is a custom function used to parse the CSP header string into a structured object. This object maps CSP directives (like ‘script-src’) to their respective values.
      policy.split(“;”).forEach((directive) => {
      const [directiveKey, …directiveValue] = directive.trim().split(/\s+/g);
      if (directiveKey && !Object.hasOwnProperty.call(result, directiveKey)) {
      result[directiveKey] = directiveValue;
      }
      });
    • Create the CSP header with the EdgeWorker dynamically generated nonce: the parsedPolicyElement obtained from the previous step, which represents the default script source value, is replaced in the originCSPHeader with the generated nonce. The resulting newCspHeader is an updated CSP header string that includes the generated nonce for script sources.
      let responseHeaders = htmlResponse.getHeaders();
      let parsedPolicyElement = parsedPolicy[‘script-src’][0].toString();
      let newCspHeader = originCSPHeader.replace(parsedPolicyElement, “'”+nonce+“'”);
      responseHeaders[‘content-security-policy’] = [newCspHeader];
  3. Rewriting HTML with the Nonce The code utilizes the html-rewriter EdgeWorker module to manipulate the HTML content of the response. It dynamically injects the generated nonce into the HTML. Any existing script elements that possess the original origin nonce are updated to use the newly dynamically generated nonce.
    • rewriter.onElement() registers a handler to run when a CSS selector matches. The callback function allows us to write code to modify the html content.
    • rewriter.onElement() matches on the origin nonce selector and the callback uses the setAttribute() method to modify the value of the nonce attribute for the matched element. The new value is set to the dynamically generated nonce.
      rewriter.onElement(‘[nonce=’ + parsedPolicyElement + ‘]’, el => {
      el.setAttribute(‘nonce’, encodedData, {quote: “'”})
      });

The Result: The nonce gets generated for every request and added to the CSP header and injected into the HTML source. The CSP Header:

And the HTML snippet:

Closing Comments: While EdgeWorkers provides numerous performance benefits for CSP, it’s essential to remember that it should complement the security measures implemented on the application server-side. A robust defense-in-depth strategy that combines server-side security, Edge security and edge-based CSP headers will ensure comprehensive protection for your web application. While caching configuration wasn’t covered in this post, it’s crucial to remember that the real value lies in the ability to apply dynamic CSP to cached content for every request. Ensure your caching setup is correctly configured to fully realize this benefit and eliminate the need for repeated trips to the origin server.

2 Likes