The following is a guest post by Scott Helme, a Security Researcher, international speaker, and blogger. He's also the founder of the popular securityheaders.com and report-uri.com, free tools to help people deploy better security.
With the continued growth of Report URI we're seeing a larger and larger variety of sites use the service. With that diversity comes additional requirements that need to be met, some of them simple and some of them less so. Here's a quick look at those challenges and how they can be solved easily with a Cloudflare Worker.
Sending CSP Reports
When a browser sends a CSP report for us to collect at Report URI, we receive the JSON payload sent to us but we also have access to two other pieces of information, the client IP and the User Agent string. We never store, collect or analyse the client IP, we simply don't need or want to, and all we do with the UA string is extract the browser name like Chrome or Firefox. Most site operators are perfectly happy with our approach here and things work just fine. There are however some issues when the site operator simply doesn't want to have us to have this information and some cases have come up where they can't allow us to have access to that information because of restrictions placed on them by a regulator. The other common thing that comes up, which I honestly never anticipated, was simply the perception of the reporting endpoint being a 3rd party address. There are various different ways we can and do tackle these problems.
CNAME
Up until now, if a client didn't want to report to a 3rd party address we would ask them to CNAME their subdomain to us and run a dedicated instance that would ingest reports using their subdomain. We take control of certificate issuance and renewal and the customer doesn't need to do anything further. This is a fairly common approach across many different technical requirements and it's something that has worked well for us. The problem is that it does come with some administrative overheads for both parties. From our side the technical requirements of managing separate infrastructure are an additional burden, we're responsible for a subdomain belonging to someone else and there are more moving parts in the system, increasing complexity. I was curious if there was another way.
HTTP Proxy
One idea that we discussed with a customer a while back, but never deployed, was for them to run a proxy on premise. They could report to their own endpoint under their own control and simply forward reports to their Report URI reporting address. This means they could shield the client IP from us, mask the User Agent string if required and generally do any sanitisation they liked on the payload. The problem with this was that it just seemed like an awful lot of work, I'd much rather have discussed deploying Report URI on premise instead. The client is also still at risk of things like accidentally DDoSing their endpoint, which removes one of the good reasons to use Report URI.
Finding Another Way
For the most part our current model was working really well but there were some customers who had a hard requirement to not send reports directly to us. Our on premise solution also isn't ready for prime time just yet so we needed something that we could offer, without it requiring too much of the overhead mentioned above. That's when I had an idea that might just cut it.
Javascript On A Plane
I was sat on a flight just a few days ago and I never like to waste time. When I'm sat in the car on the way to the airport, sat in the airport or sat on my flight, I'm working. Those are precious hours that can't be wasted and during a recent flight between Manchester and Vienna I was playing around with Cloudflare Workers in their playground. I was tinkering with a worker to add Security Headers to websites, which has since been launched, and whilst inspecting the headers object and looking through the headers that were in the request I saw the User Agent string. "Oh hey, I could remove that if I wanted to" I thought to myself, and then the rapid fire series of events triggered in my brain when you're in the process of realising a great idea. I could remove the UA header... From the request... Then the worker can make any subrequests it likes... Requests to a different origin... THE WORKER CAN RECEIVE A REPORT AND FORWARD IT!!!
I realised that (of course) a Cloudflare Worker could be used to receive reports on a subdomain of your own site and then forward them to your reporting address at Report URI.
Using Cloudflare Workers As A Report Proxy
One of the main benefits of using Report URI is just how simple everything is to do and all of the solutions mentioned at the start of this blog changed that. With a Cloudflare Worker we could keep the absolute simplicity of deploying Report URI but also easily allow you the option to shield your client's IP addresses, or any other information in the payload, from us.
let subdomain = 'scotthelme'
addEventListener('fetch', event => {
event.respondWith(forwardReport(event.request))
})
async function forwardReport(req) {
let newHdrs = new Headers()
newHdrs.set('User-Agent', req.headers.get('User-Agent'))
const init = {
body: req.body,
headers: newHdrs,
method: 'POST'
}
let path = new URL(req.url).pathname
let address = 'https://' + subdomain + '.report-uri.com' + path
let response = await fetch (address, init);
return new Response(response.body, {
status: response.status,
statusText: response.statusText
})
}
This simple worker, deployed on your own site, provides a solution to all of the above problems. All you need to do is configure your subdomain in the var on the first line and everything else will be taken care of for you. Deploy this worker onto the subdomain you want to send reports to, follow the same naming convention for the path when sending reports, and everything will Just Work(TM).
The script above is configured for my subdomain, so if I wanted to deploy this on any site, say example.com
, I'd choose the subdomain on my site where I wanted to send reports report-uri.example.com
and off we go.
https://scotthelme.report-uri.com/r/d/csp/enforce
becomes
https://report-uri.example.com/r/d/csp/enforce
The reports are now being sent to a subdomain under your own control, the worker will intercept the request and forward it to the destination at Report URI for you. In the process you will shield the client IP as we will only see the source IP as being the Cloudflare Worker and in the example above we are forwarding the UA string for browser identification.
Amazingly Simple
With the worker above we don't need to worry about setting up a CNAME, certificate provisioning, separate infrastructure or anything else that goes with it. You also don't need to worry about setting up and managing a proxy to forward the reports to us and traffic or processing power required to do so. The worker will take care of all of that and what's best is that it will take care of it with minimal overhead, taking only a few minutes to setup and costing only $0.50 for every 1 million reports it processes.
Taking It One Step Further
The great thing about this is that once the worker is setup and processing reports, you can start to do some pretty awesome things beyond just proxying reports, workers are incredibly powerful.
Downsample report volume
If you want to save your quota on Report URI, maybe you're early in the process of deploying CSP and it's quite noisy, no problem. The worker can select a random downsample of reports to forward on so you can still receive reports but not eat your quota quite as quickly. Make the following change to the start of the forwardReport()
function to randomly drop 50% of reports.
async function forwardReport(req) {
if(Math.floor((Math.random() * 100) + 1) <= 50) {
return new Response("discarded")
}
Hide the UA string
If you did want to hide the UA string from Report URI and not let us see that either, you simply need to remove the following line of code.
newHdrs.set('User-Agent', req.headers.get('User-Agent'))
Advanced work
The worker can pretty much do anything you like. Maybe there are sections of your site that you don't want to send reports from. You could parse the JSON and check which page triggered the report and discard them. You could do a regex match on the JSON payload to make sure no sensitive tokens or information get sent too. The possibilities are basically endless and what we can say is that if you need to do it, it's easy and cheap enough to do in a Cloudflare Worker.
Pricing
Talking about cheap enough, I thought I'd actually quantify that and quote the Cloudflare pricing for workers.
Starting at $5 per month and covering your first 10 million requests is an amazingly cheap deal. Most websites that report through us wouldn't even come close to sending 10 million reports so you'd probably never pay any more than $5 for your Cloudflare Worker. That's it, $5 per month... By the time you've even thought about creating a CNAME or standing up your own proxy you've probably blown through more than Cloudflare Workers would ever cost you. What's best is that if you already use Cloudflare Workers then you can roll this into your existing usage and it might not even increase the cost if you have some of your initial 10 million requests spare. If you don't use Cloudflare on your site already then you could just as easily grab a new domain name exclusively for reporting, that'd cost just a few dollars, and stand that up behind Cloudflare too. One way or another this is insanely easy and insanely cheap.