Content Security Policy (CSP)
Content Security Policy (CSP) is important as it can help you prevent cross-site scripting (XSS), clickjacking, and other code injection attacks resulting from executing malicious content. For example, malicious content can be hidden in remote code from a banner ads service you included in your app, or it could be hidden in code from a client side package you installed, which comes from a compromised NPM account.
For example, using the Content-Security-Policy HTTP header to limit fetching resources only from sources you explicitly declared can mitigate the risks related to content injection attacks. OWASP has an interesting OWASP "Content Security Policy (CSP)" cheat sheet page with examples that show you what kind of malicious attacks the different CSP directives can help prevent
CSP policy using a meta element or header
There are two ways to define CSP directives: either you use a header sent by your server, or you use the meta element in your HTML
A basic meta element to configure a CSP policy would look like this:
The header is often preferred because it supports sending reports, which cannot be done using the meta element.
A very basic Content-Security-Policy header looks like this:
Besides the Content-Security-Policy header, there is a second header Content-Security-Policy-Report-Only that can be used, the difference is that when using Content-Security-Policy the browser will enforce the policy, and if you use Content-Security-Policy-Report-Only the browser will only report violations but NOT enforce them
Here is the same example but without enforcing directives (only reporting them):
CSP directives
The default-src directive is a fallback that gets used every time a fetch directive for a resource type is missing. For a complete list, check out the MDN "CSP: default-src" documentation
I saw countless CSP examples (for beginners) that would set the default-src to 'self'. I recommend setting the default-src to none, which will disallow any source for any content type that is not explicitly whitelisted using a fetch directive.
Use a secure by default approach instead of an open everything by default approach, like this:
Always set a default-src. If you don't set a default-src, then every resource type that has no directive will be allowed, which means that if there is no default-src and you don't set the script-src directive, then all scripts from whatever source will be allowed, if you don't set a default-src there is no fallback.
Now that we have a basic CSP, I would check what violations get listed in your browser console and then use specific directives to fix the violations one at a time. For example, a directive to allow images from the same origin as the HTML document would look like this:
For a website that has images, javascript, and CSS and where the javascript code needs to make requests to a remote API (api.example.com), I would use this basic CSP policy:
Not all policy directives are fetch directives. Check out this MDN "Content-Security-Policy Directives" documentation for a complete list of directives
For example, adding the require-trusted-types-for directive is recommended to reduce the DOM XSS attack surface. This ensures that if you have a script that attempts to put a string into, for example, Element.innerHTML, but the string has not been sanitized, then CSP will consider this a violation:
nonce, hash, and strict-dynamic
An alternative to whitelisting an entire domain for external scripts, is to use hashes or nonces to whitelist only certain scripts from that source.
Those nonces and hashes can also be used for inline scripts. When used for inline scripts, the big advantage is that you can avoid using unsafe-inline, which whitelists all inline scripts.
Hashes are a powerful way of only allowing certain scripts instead of whitelisting an entire external source. This is preferred because if the domain gets taken over or hacked and the script(s) gets altered to include malicious code, the hash won't match anymore, and your code will refuse to use the script. This is much safer than whitelisting a domain and accepting whatever version of a script the domain is hosting.
Nonces are very useful for your inline scripts. Instead of using unsafe-inline, you create a nonce for your inline script(s), hence only allowing scripts that have a nonce you made instead of allowing all inline scripts. Many frameworks support nonces, for example if you use Next.js check out their "nonce" documentation or Remix "Streaming with a Content Security Policy" documentation (If you are using Remix, you might also want to dig into this Remix CSP nonce Issue #5162) and for Astro check out discussion #377
Finally, I recommend checking out the strict-dynamic source expression, which can be used when you want your trust in a script to be propagated to all the scripts getting loaded by the that "root" script. But use with caution as allowing a script to load other scripts should only happen if you have unconditional trust in the source itself, as well as the sources your source gets other scripts from
upgrade-insecure-requests and frame-ancestors
The upgrade-insecure-requests directive can be used to upgrade any first-party as well as third-party requests to https. This is useful for cases where, for example, you have an image URL that has yet to be migrated from HTTP to HTTPS or where a mistake was made by using HTTP instead of HTTPS. This directive will tell the browser to rewrite the URL with HTTPS before the insecure request gets made, but be careful. This is NOT a replacement for the HSTS header.
Here is a quote from MDN that explains it well:
The upgrade-insecure-requests directive will not ensure that users visiting your site via links on third-party sites will be upgraded to HTTPS for the top-level navigation and thus does not replace the Strict-Transport-Security (HSTS) header, which should still be set with an appropriate max-age to ensure that users are not subject to SSL stripping attacks.
So even if you use the upgrade-insecure-requests directive, it is still recommended to also set the Strict-Transport-Security header
You might still find examples on the web using the block-all-mixed-content. This one is, however, deprecated and not needed when using upgrade-insecure-requests
The frame-ancestors directive is useful to specify what parent source may embed a page and can help prevent clickjacking attacks, so if you want to disallow anyone to put your pages into an iFrame then set this directive to none, when frame-ancestors is set to none it does the same thing as the X-Frame-Options header when set to deny, meaning that the frame-ancestors directive is a replacement for X-Frame-Options in modern browsers, so the day you see a high enough support percentage on the caniuse "frame-ancestors" page then you can ditch the X-Frame-Options header
CSP online validation
A tool that can help you improve your Content-Security-Policy HTTP header is Google's CSP Evaluator, it will analyze your settings and make suggestions if it finds something that can be improved
The state of violations reporting (as of February 2024)
CSP violations can not only be seen in the console, as this would limit us to only see violations that occur when we (the developers of the project) visit our project ourselves, but what about violations that happen when a user visits our project, for such cases you can use CSP violations reporting
However, how CSP violation reports work has changed over the years. The W3C has released several iterations of its Content Security Policy recommendations / working drafts and even created a draft for the Reporting API, a new header dedicated to sending reports to an endpoint.
- in the CSP level 1 & CSP level 2 recommendations, CSP violations get reported using a report-uri reporting directive, if we look at caniuse report-uri page we can see that it is supported by all major browsers
- CSP level 3 introduces a new report-to reporting directive and the w3c has marked the report-uri directive as deprecated. Have a look at the caniuse report-to page to check how browser support evolves
All browsers support the report-uri directive, but (as of now, june 2024) NOT all browsers support the report-to directive or the Report-To and Reporting-Endpoints headers:
- Firefox supports the report-to directive but is behind a feature flag. The Firefox "CSP: Implement report-to" Issue is quite old, but one of the latest comments states that support has landed but is behind a feature flag). Support for the reporting API gets tracked using the "Considering implementing the Reporting API" Issue and first shipped in Firefox 65 but has also been behind a feature flag since
- Safari added support for the report-to directive and the Reporting-Endpoints header in their technology preview 154 and support landed in Safari version 16.4 (released March 27, 2023)
- Chrome on the other hand has had support for report-to directive since version 69 and also the corresponding Report-To header since version 69 (based on the reporting API v0 specs), since version 96 chrome now supports the more modern Reporting-Endpoints header (introduced in the reporting API v1)
report-uri
report-uri is a directive that tells the browser where to send CSP violation reports. The w3c has marked it as deprecated, but as of today, this is still the only reporting mechanism most SAAS logging services support and the only directive supported by all major browsers.
A Content Security header that uses the report-uri directive looks like this:
report-to != Report-to
If, like me, you read some documents about CSP, you might find that there are two report-to, report-to (with a small first letter r) refers to the directive, this report-to directive can point to a Reporting API header, in the first version (v0) of the reporting API that header was called Report-to (with a capital R)
The report-to directive is part of the w3c CSP level 3 reference and is still valid today
The Report-to header was part of the W3C's initial draft for the reporting API v0. However, this draft is now obsolete. If you see this header in the documentation, it means that it is outdated.
In the new w3c "Reporting API v1" working draft, the Report-to header got replaced by a header that is now called Reporting-Endpoints
Reporting API
The Reporting API describes a new header that tells the browser which endpoint(s) should get used to send the reports.
This header is NOT just for sending CSP violation reports but covers a wide range of use cases. For example, it can be used to send Permissions-Policy reports, reports regarding deprecated browser features your code might be using, reports about browser crashes, and more.
Reporting API v0
The first reporting API v0 draft of the Reporting API (v0) included a new Report-to header
chrome version 69 was the first browser to ship with support for the Report-to header and based on caniuse "Report-to header" neither Firefox nor Safari support it (yet)
An example of the Reporting API v0 (deprecated) using the report-to directive would look like this:
And using the Report-to header like this:
Did you notice the max_age key in the Report-to json? This tells the browser for how long you want to cache the endpoint information, meaning that if the next page (as long as the origin is the same) wants to make a report, you don't need to specify the endpoint again and can just use it in the directive, this feature, however, got dropped in the v1 specs of the reporting API
Reporting API v1
After a while, the w3c team decided to rename the header, and so in the w3c "Reporting API v1" working draft, they renamed the Report-to header to Reporting-Endpoints
chrome 96 is the first browser to ship support for the Reporting-Endpoints header
There is a very good article on developer.chrome.com you may want to read with lots of additional information about the new Reporting API Endpoints titled Monitor your web application with the Reporting API and they also have a Reporting API v0 to v1 migration guide
An example of a header using the new Reporting-Endpoints header for CSP violations:
You can specify more than one endpoint if you need to:
Logging CSP violations
A lot of the paid SAAS Error Monitoring services can also be used to log CSP violations, Sentry.io CSP logging documentation, raygun.com or datadoghq.com
An alternative is to host a logging tool on your own infrastructure. For example, Mozilla published an opensource CSP logging service called "CSP Logger" on GitHub that is written in Javascript, but it only supports the report-uri directive and has not been updated in years
You could also write your own CSP logging endpoint and store the reports in a database, but then you would probably also need to build an admin interface to filter and analyze the reports.
Logging CSP violations using Sentry.io
Sentry.io can be used to log CSP violations and will add those to your project issues.
Sentry.io also supports Certificate Transparency reports logging and HTTP Public Key Pinning (HPKP). However, both those features are now obsolete.
Sentry.io does not have the Reporting-Endpoints header (yet)
The reporting API v1 Reporting-Endpoints header is not yet supported by sentry.io. There are several open tickets related to this; one of them is issue #38940
Sentry.io (by default) has a filter for reports coming from localhost
Your localhost requests might get filtered by Sentry.io if you or a team member have enabled that feature; if you use report-uri, you will see the successful requests in the Network tab, but they won't show up in Sentry.io.
For a guide about how to disable the filter, check out the chapter about disabling the "reports from localhost" filter in my Sentry.io post
You probably don't want to keep the localhost reports filter disabled for too long as developing locally will generate all sorts of error logs that are not very useful, so just remember to turn the filter back on when you don't need the localhost reports anymore
The Sentry.io documentation example is not ideal
The Sentry.io documentation suggests you add the Reporting API v0 as a fallback for the report-uri directive, like so:
The problem is that Chrome (>96) will attempt to use the new report-to directive, but Chrome (>96) supports the Reporting-Endpoints header from the Reporting API v1 and not the Report-To header defined in Reporting API v0 specs. Chrome will fail to send the reports and never fall back to using the report-uri directive, meaning that if you use this suggested setup, you will not get any reports from Chrome (>96).
I suggest using the report-uri directive for the time being and adding the report-to directive only when browser support for that feature has significantly improved.
CSP debugging tips
Reduce chrome CSP reporting delay
Chrome will wait some time to ensure it has collected all the potential CSP reports before attempting to send them to the reporting server. However, there is a command-line switch that you can use to shorten that delay when debugging CSP reporting.
This option is NOT a Flag. It is a command-line option.
On Mac or Linux, to shorten this delay to a minimum, you can use the following command in your terminal to start Chrome manually:
On Windows, if you use PowerShell, then the command syntax is a bit more complex:
report-uri works in localhost, but report-to does NOT
When using report-uri, reporting violations work locally (localhost) as well as in preview/production (secure context / https URL)
When using report-to, reporting violations will NOT work on localhost (in Chrome) without a valid SSL certificate, which is surprising as localhost usually is considered a secure context (if you know why, please let me know using the chris.lu github discussions)
In staging (preview) / production environments, however, the report-to will work if those environments have an SSL certificate.
report-uri requests show up in the Network tab
When using the report-uri directive, you can inspect requests using the developer tools Network tab like any other POST request made by your app.
report-to requests are Not in the Network but in the Application tab
When using the report-to directive, you won't see any entries in your developer tools Network tab. This is because a background service makes the request(s) and not the website (your code)
To see the requests, open your developer tools, then click on the Application tab, and then on the left list under Background services, click on Reporting API:
Adblockers may block CSP violation logging services
If you are using report-uri and your logging tool does not receive the reports, then they might be blocked. To verify if the requests are blocked, open the developer tools in your browser and then go into the network tab. Look at the request rows and check if their status is blocked. In this case, you probably have an adblocker extension installed that blocks the requests. I, for example, use Privacy Badger and have to allow the domain (of the service I send reports to) in the extension settings
So make sure to either whitelist the Sentry.io CSP reporting URL or disable the ad blocker for your domain, or even disable it completely until you are done testing.
Chrome (Chromium) CSP preflight request headers BUG
This bug is important if you want to create your own CSP logging server.
A bug in Chrome (Chromium) will cause Preflight requests to fail if some of the response headers use a wildcard.
Chrome will send a request to your server using the OPTIONS method to test your endpoint before sending the actual reports, but if your server response has headers with an Asterisk, then the preflight request will fail.
So, for example, these headers will NOT work:
But these will work:
Have a look at the chromium issue #40733909 if you want more details, and maybe you are lucky, and it got fixed
Create a quick and dirty CSP reporting server
To help me better understand what kind of information browsers send when using the different versions of CSP reports, I wrote a small Node.js server tool to display some information about CSP logging calls.
If you want to set up a CSP logging server for yourself, you can use the following code to get started:
To start the server, use the following command:
Debugging Sentry.io dropping reports
While testing the CSP reporting feature on Sentry.io, I had trouble understanding why some reports would not go through.
In the Stats page on Sentry.io, I would only see that a certain number of requests had been dropped, but I would not know why.
If you want to get a little bit more information as to why requests get dropped, you can use the Sentry.io API. I recommend having a look at the Sentry.io API chapter in my Sentry.io Post
Debugging Chrome requests using Netlog
Before you start experimenting with Chrome Netlog, know that, as of now, the information you get from the logs is not very helpful. You still can't see the exact reason as to why a CSP report call has failed, but hopefully, future versions will bring improvements. This is why I added this option to this post.
Debugging the reporting API in Chrome is painful as you don't get any information about why requests have failed. If they fail, The requests will return to a Queued state, and Chrome will retry to send the report. If sending the reports fails for a while, chrome will show a "MarkedForRemoval" state
Chrome (Chromium) has a tool called Netlog:
NetLog is an event-logging mechanism for Chrome’s network stack to help debug problems
You can access Netlog by entering the following URL into your address bar:
Now click on the Start Logging To Disk button, and then in another tab, open the website, making the CSP reporting calls.
Then, after a while, click on Stop Logging.
To analyze the content of the log file, head over to the Netlog viewer website that Google has created to help you analyze logs
Then, in the left navigation, click on Reporting.