Content-Security-Policy HTTP header
How the Content-Security-Policy header controls which resources a page can load, its key directives, report-only mode, and how to set it in Apache, PHP and AWS CloudFront.
The Content-Security-Policy header is the most capable of the HTTP security headers. It lets you tell the browser exactly which sources of content - scripts, styles, images, fonts and frames among them - are allowed to load on a page, and it is one of the strongest defences available against cross-site scripting (XSS) and content injection.
Where X-Frame-Options can only control framing, Content-Security-Policy covers framing and a great deal more, and its frame-ancestors directive is the modern replacement for X-Frame-Options altogether. It sits alongside the other headers as part of a wider approach to web application security.
Like all the HTTP security headers, it is set in the HTTP response by your web server, application or content delivery layer.
How a policy is built
A policy is a list of directives separated by semicolons. Each directive names a resource type and the sources the browser may load it from.
Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'
A few of the most useful directives:
default-src- the fallback source list for any directive you do not set explicitly.script-src- where scripts may load from, and the single most important directive for stopping XSS.style-src,img-src,font-src,connect-src- styles, images, fonts, and the endpoints thatfetch,XMLHttpRequestand WebSockets may reach.frame-ancestors- which sites may frame yours.'none'is the equivalent ofX-Frame-Options: DENY.object-src 'none'- blocks legacy plugin content, and worth setting on almost every site.
Common source values are 'self' for the site's own origin, 'none', a specific host such as https://cdn.example.com, and data: for inline data URIs. Avoid 'unsafe-inline' and 'unsafe-eval' where you can, since they undo much of the protection. Where an inline script is genuinely unavoidable, prefer a nonce or a hash over opening the policy up.
Testing a policy without breaking the site
A strict policy will often block something the first time you apply it. Use the report-only variant to see what would be blocked without actually blocking it:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
The browser reports violations to the endpoint you name rather than enforcing the policy, so you can tighten it against real traffic before switching to the enforcing header.
Setting it on AWS CloudFront
For a static site behind CloudFront there is no web server to set the header on, so it goes on the distribution instead, and this is how I set it on the static sites I host in S3. The cleanest option is a response headers policy, a managed CloudFront feature where you declare your security headers once and CloudFront attaches them to every response with no code to maintain. AWS provides a managed security headers policy that covers several of the common headers, though the Content-Security-Policy itself you define yourself, since it is specific to your site. Where you need something the policy cannot express, a CloudFront Function on the viewer response can add or rewrite headers in a few lines of JavaScript at very low cost, and Lambda@Edge is worth reaching for only when you genuinely need it, for instance generating a fresh nonce per request to fold into a stricter script-src.