BlogDeploy

Creating a CSP report-uri endpoint with Cloudflare functions

Written by Codemzy on April 12th, 2024

Here's how I used Cloudflare functions to create a "report-uri" endpoint for my CSP policy. The Cloudflare worker gets the CSP violation report and logs it so I can review and react!

For a while, I sent CSP reports to an endpoint on my server, and they got logged with the rest of my API logs. And that all worked ok when I only had a few CSP reports, by occasionally I would get big spikes in CSP reports due to a user's browser extension or something like that.

Since my production logs are sent to long-term storage, I wanted to separate my CSP logs, as I didn't need these logs stored long-term or cluttering up my other logs that were more based around user actions and requests.

I also thought it would be good to send the CSP violation reports directly where I needed them logged rather than hitting my server first. My server was just forwarding them on, and I didn't want potential big spikes in CSP violations to slow down the server.

Even if I filter out some CSP violations that I can't do much about, the requests will still need to hit my server so I can do the filtering.

So I had a look at some dedicated services that would provide me with a "report-uri" endpoint. Like Sentry, and Report URI.

And that all looked pretty good, but with the amount of CSP reports I can get (which can be caused by something out of my control like a user's browser extension), I was worried about spikes in reports pushing me into higher pricing plans.

Because there can be a lot of CSP violations that are just unactionable. Like from browser extensions and add-ons.

There's no easy way to throttle these CSP violation reports either, although rate-limiting CSP reporting has been discussed.

Since I already host my static site on Cloudflare, I thought about using workers/functions for my "report-uri" endpoint. And I could use that function to send a log to my existing log management service (BetterStack).

It would solve most of my issues:

  • I can take the load off my server so any spikes in CSP reports don't slow down critical functions
  • I can avoid using an external service where spikes in CSP reports might blow through the monthly allowance
  • I can add rate limiting to the function through Cloudflare if needed
  • I can keep all my logs in one place

No extra services, no extra plans.

I can even create a new source in BetterStack just for my CSP logs. I might even spin up a dashboard to visualise some of the data (and get alerts) too!

Ok so here's the plan:

  • use a Cloudflare function as the report-uri endpoint
  • create a new source in BetterStack for my CSP logs
  • send the CSP violation reports to my logs

Cloudflare function for report-uri

You'll need a Cloudflare worker you can route to for the "report-uri" endpoint. The report-uri directive is deprecated but still used by most browsers.

The deprecated HTTP Content-Security-Policy (CSP) report-uri directive instructs the user agent to report attempts to violate the Content Security Policy. These violation reports consist of JSON documents sent via an HTTP POST request to the specified URI.

- MDN CSP: report-uri

We'll set up Report-To as well to future-proof our CSP for the newer directive when all browsers support it!

Content-Security-Policy: …; report-uri https://www.example.com/api/report-csp; report-to csp-endpoint
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://www.example.com/api/report-csp"}],"include_subdomains":true}

If you are using Cloudflare Pages, you can set up a functions directory in your project to use Cloudflare Workers.

I'm doing that, so I created a folder like this:

📁 functions/
└── 📁 api/
  └── 📄 report-csp.js

And now my report-uri endpoint is ready!

Well, not quite. It doesn't do anything yet! Let's fix that.

// 📄 functions/api/report-csp.js
export function onRequestPost(context) {
  // do something
}

I'm using onRequestPost because I only want this function to handle POST requests since that's what is sent for a CSP violation.

The first thing we'll need to do is get the body of the request as JSON. You'll find the request on the context object. Cloudflare Workers use the Fetch API, so to get the body (the data sent as the CSP violation report) we need to parse the request with .json().

Here's how that looks:

// 📄 functions/api/report-csp.js
export function onRequestPost(context) {
  const data = await context.request.json(); // data from body
}

Nice!

Now we need to do something with that report!

Log the CSP violation report

I'm going to send the report somewhere so I can log all the CSP violations, and check them. I can update my CSP policy if I identify any resources that are being blocked by mistake, and notice issues early by getting alerted if there's a sudden increase in CSP reports.

Since I'm using BetterStack, I'll send the logs there. But I'll use the Fetch API to send the request, so you can tweak this for whatever service you are using.

In BetterStack, I've created a new HTTP Rest API source so I can send the log with an HTTP request.

And I'll add the source token as an environment variable in Cloudflare.

Now I can update my function to send the log:

// 📄 functions/api/report-csp.js
export function onRequestPost(context) {
  const data = await context.request.json(); // data from body
  // send csp violation to logs
  return await fetch("https://in.logs.betterstack.com", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${context.env.LOGS_CSP_SOURCE}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ 
      message: "CSP Violation", 
      'csp-report': data['csp-report'],
    }),
  });
}

I can also send extra information like the 'user-agent' and IP headers, in case I need to take action - like blocking or rate limiting an IP from this endpoint - or even the entire site.

// 📄 functions/api/report-csp.js
export function onRequestPost(context) {
  const data = await context.request.json(); // data from body
  // send csp violation to logs
  return await fetch("https://in.logs.betterstack.com", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${context.env.LOGS_CSP_SOURCE}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ 
      message: "CSP Violation", 
      'csp-report': data['csp-report'],
      browser: context.request.headers.get('user-agent'),
      ip: context.request.headers.get('CF-Connecting-IP'),
    }),
  });
}

Cool! Now I have a Cloudflare Worker handling the "report-uri" endpoint.