Content Security Policy with Amazon CloudFront: Part 2
We wrap up this series and provide a final configuration that gives us a perfect score.

This a continuation of the series starting with, Content Security Policy with Amazon CloudFront: Part 1.
Clickjacking
Having implemented Strict-Transport-Security, Mozilla Observatory reports:
What’s a good next step?
The use of the X-Frame-Options header and Content Security Policy’s frame-ancestors directive are a simple and easy way to protect your site against clickjacking attacks.
— Mozilla Observatory
To keep things simple, we restrict our web application to not load in an iframe with a number of HTTP response headers.
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY
We update the Amazon Lamda function (re-creating the CloudFront distribution and CNAME records) as follows:
'use strict';exports.handler = (event, context, callback) => {
...
const headerHSTS = 'Strict-Transport-Security';
const headerCSP = 'Content-Security-Policy';
const headerXFO = 'X-Frame-Options';
...
headers[headerHSTS.toLowerCase()] = [{
key: headerHSTS,
value: 'max-age=31536000',
}];
headers[headerCSP.toLowerCase()] = [{
key: headerCSP,
value: 'frame-ancestors \'none\'',
}];
headers[headerXFO.toLowerCase()] = [{
key: headerXFO,
value: 'DENY',
}];
callback(null, response);
};
With this simple improvement we dramatically improve our score.

Valid Mime Types
You’re halfway finished! Nice job!
The X-Content-Type-Options header tells browsers to stop automatically detecting the contents of files. This protects against attacks where they’re tricked into incorrectly interpreting files as JavaScript.
— Mozilla Observatory
Only halfway finished?!
A more complete description of this feature is:
The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed. This allows to opt-out of MIME type sniffing, or, in other words, it is a way to say that the webmasters knew what they were doing.
— Mozilla — X-Content-Type-Options
The header implementation is…
X-Content-Type-Options: nosniff
We update the Amazon Lamda function (re-creating the CloudFront distribution and CNAME records) as follows:
'use strict';exports.handler = (event, context, callback) => {
...
const headerCTO = 'X-Content-Type-Options';
...
headers[headerCTO.toLowerCase()] = [{
key: headerCTO,
value: 'nosniff',
}];
callback(null, response);
};
And we go from a B- to a B.

Content Security Policy
The feedback the Observatory gives is:
You’re doing a wonderful job so far!
Did you know that a strong Content Security Policy (CSP) policy can help protect your website against malicious cross-site scripting attacks?
In a nutshell, Content-Security-Policy HTTP response header controls which resources an HTML page can use. Reading up on this, a recommended starter policy is:
Content-Security-Policy: default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';
This creates a default that blocks content and then allows exceptions for scripts (JS), connects (XHR), images (Img), and styles (CSS).
Using Chrome developer tools, we can see that our sample app only needs scripts, images, and styles (I thought) from the same domain.

When I first tried this, I thought I could removed the connect (XHR) exception and discovered that the service worker setup with create-react-app reported errors (apparently it is setup to pre-load files); a sample error:
service-worker.js:1 Refused to connect to 'https://d1ivzly8he3uqo.cloudfront.net/index.html?_sw-precache=cef06143bd734e39a03a769077b815dc' because it violates the following Content Security Policy directive: "default-src 'none'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.
We update the Amazon Lamda function (re-creating the CloudFront distribution and CNAME records) as follows:
'use strict';exports.handler = (event, context, callback) => {
...
headers[headerCSP.toLowerCase()] = [{
key: headerCSP,
value: 'default-src \'none\'; script-src \'self\'; connect-src \'self\'; img-src \'self\'; style-src \'self\';',
}];
...
};
Yeah, we got our A+.

But, of course the tool gave us some more feedback (since when is A+ was not perfect).
Referrer Policy
You’re on the home stretch!
The use of Referrer Policy can help protect the privacy of your users by restricting the information that browsers provide when accessing resources kept on other sites.
— Mozilla Observatory
To completely stop the browser from sending referrer headers when leaving our web application, we use:
Referrer-Policy: no-referrer
We update the Amazon Lamda function (re-creating the CloudFront distribution and CNAME records) as follows:
'use strict';exports.handler = (event, context, callback) => {
...
const headerRP = 'Referrer-Policy';
...
headers[headerRP.toLowerCase()] = [{
key: headerRP,
value: 'no-referrer',
}];
callback(null, response);
};
Finally

The final full Amazon Lamda function is:
'use strict';exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
const headerNameSrc = 'X-Amz-Meta-Last-Modified';
const headerNameDst = 'Last-Modified';
const headerHSTS = 'Strict-Transport-Security';
const headerCSP = 'Content-Security-Policy';
const headerXFO = 'X-Frame-Options';
const headerCTO = 'X-Content-Type-Options';
const headerRP = 'Referrer-Policy';
if (headers[headerNameSrc.toLowerCase()]) {
headers[headerNameDst.toLowerCase()] = [{
key: headerNameDst,
value: headers[headerNameSrc.toLowerCase()][0].value,
}];
console.log(`Response header "${headerNameDst}" was set to ` +
`"${headers[headerNameDst.toLowerCase()][0].value}"`);
}
headers[headerHSTS.toLowerCase()] = [{
key: headerHSTS,
value: 'max-age=31536000',
}];
headers[headerCSP.toLowerCase()] = [{
key: headerCSP,
value: 'default-src \'none\'; script-src \'self\'; connect-src \'self\'; img-src \'self\'; style-src \'self\';',
}];
headers[headerXFO.toLowerCase()] = [{
key: headerXFO,
value: 'DENY',
}];
headers[headerCTO.toLowerCase()] = [{
key: headerCTO,
value: 'nosniff',
}];
headers[headerRP.toLowerCase()] = [{
key: headerRP,
value: 'no-referrer',
}];
callback(null, response);
};
Enjoy.