Static Site Security Posture

Using Lambda@Edge to Implement OWASP Secure Headers for S3 Hosted Websites

Posted by Mike Apted on Tuesday, August 15, 2017

In this post

The combination of S3 and CloudFront offers a low cost and easy way to deliver static and client side websites. In this blog post I’ll explore how to use Lambda@Edge to improve the security posture of your S3 hosted site through the addition of the OWASP recommended browser security headers.

There are some aspects of control that you don’t have, when using S3 and CloudFront, compared to serving your content from a more typical server environment. One feature is the ability to customize HTTP headers. A common use case is to add browser security headers in an effort to improve your site’s security posture, through additional defense in depth.

Solution Overview

The solution outlined in this post will use Lambda@Edge to add browser security headers to the Viewer Response event from CloudFront back to the user. The following diagram illustrates the process covered in this post:

Here is how the process works:

  1. A user requests content from a CloudFront distribution. It is matched to a behaviour or falls through to the default behaviour.
  2. If that object is not in cache, or is expired, CloudFront retrieves the object from S3.
  3. CloudFront executes any versioned Lambda function attached to the Viewer Response event before passing the object back to the user. The Lambda function updates the response object with additional browser security related headers.
  4. The modified response, whether served from the origin or from cache, is returned to the requesting user.

Why are security based headers important?

When considering web application security, first thoughts are usually around server level issues like SQL injection (user input sanitization), Cross Site Request Forgery and vulnerabilities in underlying code. The concept of defense in depth, however, means finding improvements to your security posture at all layers of your application, and that includes the browser.

Browser security headers can be used to instruct modern browsers how to load content and what behaviours are allowed. For example, you can include a header that prevents your website from being included via a frame in another site, or that dictates what valid sources for different types of resources (e.g., scripts, CSS, or images). OWASP Secure Headers Project

The OWASP Secure Headers Project provides a resource for helping people better understand HTTP headers, which headers are related to security, what current browser support is for those headers, and recommend values for achieving optimal security posture.

Each header must be evaluated for appropriateness in your use case, and some headers have more serious impact if misconfigured than others. So as always, use good judgement and make incremental changes, preferably on a test website before moving those changes to a production environment.

What are the OWASP recommended security headers and what do they accomplish?

Let’s run through the browser headers related to security, what each header accomplishes, and what the recommended OWASP value is.

X-Frame-Options

The X-Frame-Options header helps improve the resiliency of your site to “clickjacking” attacks. It instructs the browser on whether it should load the site as frame content in another site. Possible values are to deny all loading in frames, allow if origin domain matches, or specify a specific domain which is allowed to load the site in a frame. Barring a specific need or use case otherwise the recommended value is “deny”.

X-XSS-Protection

The X-XSS-Protection header tells the browser to enable Cross Site Scripting (XSS) filters. Possible values are to disable this feature, enable it with sanitization of detected attacks, enable it with blocking of detected attacks, and lastly enable it with sanitization and reporting. The recommend value is to enable it with blocking of detected attacks.

X-Content-Type-Options

The X-Content-Type-Options header will instruct the browser not to use MIME-sniffing and interpret a response as something other than the Content-Type header. This prevents the browser from possibly executing JavaScript embedded in a non-JS file, for example. There is only one possible value here, the recommended option of “nosniff”.

Referrer-Policy

The Referrer-Policy header instructs the browser on which referrer information should be included when requests are made, for example when you click a link on the page. This helps prevent leaking sensitive information to external sites and into log files. There are many possible values here based around origin matching and transport security, but the recommend value disables all referrer information and is “no-referrer”.

HTTP Strict Transport Security (HSTS)

The HSTS header instructs the browser that it should not load insecure connections to the site. This helps mitigate downgrade attacks, where a user is tricked into loading an insecure page on the site, and cookie hijacking (where session data often exists) on an insecure request. The value provided indicates the time (in seconds) that the browser should remember that this site insists on a secure connection, as well as whether it should apply to all subdomains. The recommended value is “max-age=31536000 ; includeSubDomains”.

Public Key Pinning Extension for HTTP (HPKP)

The HPKP header instructs the browser to only accept certificate chains that include one of a specific list of certificates (communicated as a public key hash value). This header carries the most risk, as misconfiguration lock users out of your website for the length of time defined in the header value.

There are different strategies, with varying levels of implementation risk vs protection involving which keys to pin. This can involve choosing between leaf, intermediate or root certificates and possibly multiple certificate authorities. Evaluation of your use case should inform your strategy here.

Content-Security-Policy

Content Security Policies (CSPs) in depth, is a blog post all its own, but this header allows fine grained control over where resources are allowed to be loaded from. It can prevent loading of malicious external JavaScript for example, or prevent execution of an XSS attack. The OWASP recommended value is “script-src ‘self’” which prevents externally hosted JavaScript from loading. If you are pulling in JavaScript resources from a CDN those would need to be added to the CSP as well.

CSPs can be very complex and if implemented incorrectly can impact the loading and rendering of the page, but they are an excellent tool in improving your site’s security posture. A detailed and well-crafted CSP will provide a greatly enhanced security posture.

Using Lambda@Edge to add browser security headers

Creating our Lambda function

The first step in the process is to create our Lambda function that will be modifying the headers in the response to the end user. You will need to ensure you are performing these actions using a user with sufficient permissions. The necessary permissions are outlined in the Lambda@Edge documentation.

In the ‘us-east-1’ region create a new Lambda function, select “Author from scratch” and refrain from attaching a trigger at this point.

Give your function a meaningful name and description, and select Node.js 6.10 as your Runtime.

Your objective in the Lambda function is to load the CloudFront response object from the Lambda event, find the headers, and then add a set of new headers that capture the increase in security posture appropriate for your use case.

Replace the default code with the following, modified to meet your needs:

'use strict';
exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const headers = response.headers;

    headers['x-frame-options'] = [{
        key: "X-Frame-Options",
        value: "deny"
    }];
    
    headers['x-xss-protection'] = [{
        key: "X-XSS-Protection",
        value: "1; mode=block"
    }];
    
    headers['x-content-type-options'] = [{
        key: "X-Content-Type-Options",
        value: "nosniff"
    }];
    
    headers['referrer-policy'] = [{
        key: "Referrer-Policy",
        value: "no-referrer"
    }];
    
    headers['strict-transport-security'] = [{
        key: "Strict-Transport-Security",
        value: "max-age=2592000; includeSubdomains"
    }];
    
    // Pinned keys are the intermediate (Symantec Class 3 Secure Server CA - G4) 
    //   and root (VeriSign Class 3 Public Primary Certification Authority - G5)
    headers['public-key-pins'] = [{
        key: "Public-Key-Pins",
        value: "pin-sha256='9n0izTnSRF+W4W4JTq51avSXkWhQB8duS2bxVLfzXsY='; pin-sha256='JbQbUG5JMJUoI6brnx0x3vZF6jilxsapbXGVfjhN8Fg='; max-age=60; includeSubDomains"
    }];
    
    headers['content-security-policy'] = [{
        key: "Content-Security-Policy",
        value: "script-src 'self';"
    }];
    
    callback(null, response);
};

Attach an appropriate role for your Lambda function execution, ensuring access to CloudWatch Logs for example. You must also ensure your role can be assumed by the service principals lambda.amazonaws.com and edgelambda.amazonaws.com. Once you have selected or created your role, then save your function.

Testing our Lambda function

Testing your function’s execution is important. A misconfigured function, once attached to your CloudFront behaviour(s), may prevent your viewers from receiving a response. There is a built-in test event in Lambda called “CloudFront Access Request Header in Response Event” that will provide a sample event perfect for our testing purposes.

Use that sample event, and validate that your Lambda function runs successfully, and that it is returning a well-structured object with the contents you expect.

Once that is achieved, you must publish a new version of your function, by selecting the Actions drop down and “Publish new version”.

Take note of the version number as you will use that when configuring CloudFront.

When testing your deployed Lambda@Edge functions note that any logs from console.log() in the function will be placed in a CloudWatch Logs group in the region closest to the function invocation. The log group name will take the format “/aws/lambda/us-east-1.” in each region.

Attaching our Lambda function to CloudFront

Once you are satisfied with the results of your Lambda function, and have published a new version, it is time to attach that function to your CloudFront distribution. The Lambda@Edge functions are attach to CloudFront distribution behaviours, which specify how CloudFront treats requests for certain URL patterns, or the default behaviour.

Select your distribution, and under the Behaviors tab, select the behavior you would like to attach the Lambda function to and click “Edit”.

At the bottom of the Behavior configuration you can attach a specific Lambda function and version, specified via it’s ARN and version number, to one or more Event Types. In our case, select Viewer Response, and then enter the ARN and version number from the top right-hand side of the Lambda console when viewing the correct function and version. For example:

arn:aws:lambda:us-east-1:<accountId>:function:<functionName>:<version>

It will take a short amount of time for the Lambda function to be replicated to all regions and attached to the behaviour, at which point you will start seeing the additional headers in new requests to the CloudFront distribution.

Cost, performance and other considerations

Lambda@Edge is billed in a similar fashion to Lambda. You are charged on a combination of total requests and function execution time. Currently, Lambda@Edge functions are billed at a granularity of 50ms. Unlike Lambda, there is no free tier. This will add a small cost to each request to your site, albeit very minimal. There will also be a small latency impact, running this function with each request, but for a function like this in my testing it was typically sub 100ms and often times single digit milliseconds. You will have to evaluate the impact on your use case and the trade off with increasing your security posture

Lambda@Edge functions have several other restrictions. They must be written in Node.js 6.10 or NodeJS 8.10, are limited to either 1 or 3 seconds (depending on which events they respond to) and are capped at 128MB of memory. You can’t configure your Lambda function to access resources inside your VPC.

While modern browsers support the majority of these headers, there are gaps, and older browsers may not support them at all. For browsers that do not support one (or many) of these headers, the browser will simply ignore the header in question. While this means no impact to the usability of the site, it also means that those users will not benefit from the increase in security posture, so as always, defense in depth applies.

It is worth stressing again that some of these headers can have an adverse and lasting impact on your site’s delivery to users if not carefully implemented, especially HPKP, HSTS and CSPs, so always evaluate your header settings with low initial max-age values and in a test environment so you understand what the impacts are and how they apply to your use case.

Summary

In this post you have seen how it is possible to use Lambda@Edge to enhance the security posture of your S3/CloudFront hosted websites, by adding browser security headers.

Some of the browser headers discussed have deep flexibility, especially Content Security Policies, and you can learn more about the various topics covered in this blog post here: