![]() |
Content Security Policy (CSP) lets you control which content is allowed to load on a page. It has a reporting feature which is intended to help you identify misconfigurations or missing sources in your policy. When the browser detects a violation of your policy, a report will be sent to the reporting endpoint(s) that you have configured.
When developing a policy, reporting helps you identify all the sources on the pages you didn’t think about. When you enforce a policy, it enables you to fix any policy problems without having to rely on users reporting them or customers escalating errors.
I have, however, found CSP reporting to be very useful in reporting other events in the user’s environment. I have implemented CSPs with reporting just to catch these events. Here are my examples in somewhat chronological order.
Chasing down mixed content
As Chrome would start to flag non-secure content in 2018, I worked hard to remove http content in the years before, a story told here. While most of this could be done with tools scanning through user content, some sources needed a different approach. For this I employed the following policy on relevant servers with “Content-Security-Policy-Report-Only” to not block anything, just report:
img-src 'self' data: https:;
script-src 'self' data: blob: https: 'unsafe-inline' 'unsafe-eval';
media-src 'self' https:;
font-src 'self' data: https:;
report-uri https://{sub}.report-uri.com/r/d/csp/reportOnly
As a security policy, this is close to useless as almost everything is allowed under this policy. The only thing that isn’t allowed is http, and every time something would be loaded on http, I would get a violation report. This worked very well. We were able to identify all the http content and update the references. I’ve also told this story in more detail before.
Checking internal devices
Most of the reports you receive on a regular tuned CSP will be noise. There will be in app browser scripts, translators, and browser extensions that trigger CSP violations. But there will also be reports due to page modifications by VPNs, malware, and security products. Some of the those are malicious actions that could indicate infected workstations.
I read about someone catching an infected device they were able to track down due to a CSP being set on an internal resource. As I had seen a lot of these events in the external user base, I knew what to look for and wanted to check this inside the company network.
I wanted to add a CSP to an internal application used by as many people as possible in the company. I ended up with the on prem Confluence server. Unfortunately, it was not possible to apply a CSP directly, so we had to proxy the connection to apply our own headers. After a period of learning what was normal for Confluence and tuning the policy, we were ready for action.
And the result was… nothing. For a long time. But suddenly there was one violation that was clearly suspicious. IT tried to track down the device based on their logs, but they couldn’t find it, which most likely meant that this was not a company owned device. Likely a privately owned device connecting through VPN to the company network.
So even though we didn’t find anything juicy, we could establish that we didn’t see a lot of suspicious activity on company devices either.
What is framed
With CSP frame-ancestors you can block framing of your pages, just like X-Frame-Options, but more flexible. With CSP reports you can check when a framing attempt is made from other origins. Our clients would sometimes frame their content on our servers. They could disallow framing of their content if they don't need it, and failure to do so often appeared in pen tests. I wanted to check if we could block framing more aggressively. For this a quite simple CSP in report only mode is needed
frame-ancestors 'self'; report-uri <report endpoint>
With hundreds of thousands of page loads a day and an unknown number of those being framed, there was a possibility of a deluge of violation reports. The header was thus only set on some of the production servers. We could see that framing occurred, but doing something about it required an investment which wouldn’t bring the necessary return on investment.
Who is framing
When you receive a violation report due to frame-ancestors, you only see which page is being attempted framed, you don’t see who is trying to frame it. This is for security and privacy reasons. If the framing is part of an authentication process, credentials can leak in the report. If the framer is a sensitive site, one that you wouldn’t like other people knowing that you were using, and you could identify users based on the framed URL, that could be a problem.
I didn’t have any of those challenges in my case, but I needed to find the hosts that were framing. So, I found a way to sneak around the restrictions:
- Take the referrer of the request
- Extract the host from the referrer
- Set a dynamic response header: “content-security-policy-report-only: frame-ancestors 'self'; manifest-src <host of referrer>; report-uri <report endpoint>;”
Restricting frame-ancestors meant that violation reports would be sent for everyone on a different origin framing the page. By adding the hostname to an unused directive, I was able to extract the relevant host names that tried to frame. As I didn’t want to expose the client names, I made a list of acceptable hosts and added them to the enforced frame-ancestors list when they matched the referrer of the request.
Broken links
In my big side project we had moved to a new web page and consequently most URLs had changed. We didn’t want to keep losing visitors being sent to the 404 page. We could see that there were many hits on it, even after setting up several redirects. On the 404 page I suggested that visitors could contact us and tell us where they were going. I even tried to sweeten the deal by promising a discount in the shop. But to no effect.
As the page already had a CSP I realized that I could be notified by violating it. So now the 404 page will attempt to load a script from a non-existent and non-allowlisted subdomain. This will trigger a CSP violation, and as the URL given in “document-uri” of the report is the original URL (again for security reasons), I can now see which URL the user tried to access. Then I can set up a redirect from the old page that the user tried to access to the new URL.
Gathering statistics
In another side project, a story told in Norwegian here, I wanted to collect statistics on usage. The page had an extremely simple tech stack:
- 1 HTML file
- 1 CSS file
- 1 JS file
- 1 .htaccess file
- Some image files
- Loading data from the external public source api.entur.io
The intention was to make this page extremely lightweight, fast, and cheap. I could probably have gotten statistics from a web server, but just hosting files was much easier. I realized that I could once again just violate the CSP. By putting the relevant usage statistics into the path of a request violating the policy I could collect the data from an external reporting endpoint I already used. As I didn’t want too many reports, I configured it to only send data after set number of route lookups. Which was probably set too high as it mostly reported on my own usage only…
WAF notices
I had another idea which I haven’t tried out. My problem is that I’m tuning a web application firewall (WAF) and is about to put it in enforce mode. When the WAF rules are enforced, we should have a quick turnaround time on adjusting the rules when legitimate usage is blocked. But who should monitor the rules? And how can we determine if requests are legitimate or correctly flagged or blocked by the WAF?
As the application is hosted in different cloud instances, we might need to check logs from multiple instances. Again, I hoped that CSP reporting could come to the rescue. As we can modify the 403 page returned when the request is blocked by the WAF, that page could contain a CSP violation triggering a report. And if that violation only was present for logged in users (likely based on a separate cookie set on login), we would know when real users hit a problem while filtering out scanners looking for weaknesses. Those scanners contribute to 99% of the WAF log entries.
While this approach is good in theory, most of the legitimate traffic being stopped by the WAF are API requests. And those responses won’t be rendered in the browser, and the violation report won’t be sent. In this case the reports will only cover some cases, and to be effective, another solution needs to be found for API requests.
Conclusion
In a lot of these cases it might seem like the reporting was a waste as it didn't produce much. But it was often just used to confirm if something was occurring or not. In some cases it provided mission critical information. And the reporting was much easier to implement than any other type of logging.
CSP reporting can be used for far more than just reporting that your policy might need an adjustment. Familiarize yourself with CSP reporting. Then get creative and use it as an additional tool in your belt whenever you need to collect some ad hoc data about your users (while respecting their privacy and security).
No comments:
Post a Comment