Getting Started With a Content Security Policy

I recently needed to setup Content Security Policy (CSP) on a website and I couldn’t think of where to get started. The first question that came to mind was what all content do I allow and how do I test everything without having to look through all of the code on the site. This is where the Content-Security-Policy-Report-Only header can come into play. The short version is that this allows you to create a policy in report only mode and you can collect the results at the endpoint specified via the report-uri directive. That’s great! I have what I need but how do I collect what’s being reported by the clients to the report-uri and what do I use for the report-uri? This was a great place for me to begin testing out DigitalOcean Functions.

Initial Environment Setup

I like to use Splunk in my environment for some of my logs so I thought this was a great way to try and gather the data. I won’t cover the full setup here but I setup a Splunk HEC on my Splunk specifically for this. You can read more about Splunk HEC here in the Splunk documentation. With the Splunk HEC configured, I have my Splunk HEC URL and Splunk HEC Token and I’m ready to move on.

Creating the Function

With my Splunk credentials in hand, it’s now time to create a really simple function that can collect the csp reporting details. I took to my DigitalOcean account and followed some of their examples in the Functions documentation. The end result is the following Python script:

import os
import requests

def send_to_splunk(msg):
    if not msg:
        return "fail"
        
    myobj = {
        'event' : msg
    }
    headers = {'Authorization' : 'Splunk ' + os.environ.get("splunkHECToken")}

    try:
        x = requests.post(os.environ.get("splunkUrl"), json = myobj, verify=False, headers=headers)
        return "ok"
    except Exception as e:
        return "error"

def main(args):
    body = send_to_splunk(msg = args)
    return {"body": body}

The nice thing about these functions is that they already have a number of useful libraries installed such as requests. The main() function is automatically called whenever the function is called. The args contain headers and get/post parameters. I’ve also created two environmental variables in this function, splunkHECToken and splunkUrl

DigitalOcean Example of Environmental Variables

Updating Nginx to Add The Header

In order to make sure I add this to all requests to the site, I configured my Nginx configuration to add the response header via the add_header directive. I simply copied the URL for the function from the Source tab in my DigitalOcean Control Panel

DigitalOcean Function URL Example

With that URL in hand, I then updated my Nginx configuration to send the header

add_header Content-Security-Policy-Report-Only "default-src https:; report-uri https://faas-sfo3-7872a1dd.doser...";

I then restarted Nginx and tested out a few requests.

Reviewing the Results in Splunk

After some time, I did some checking in Splunk to see if I’m getting results. Here is an example log:

{
  "__ow_method": "post",
  "__ow_body": "eyJjc3AtcmVwb3J0Ijp7ImRvY3VtZW50LXVyaSI6ImFib3V0IiwicmVmZXJyZXIiOiIiLCJ2aW9sYXRlZC1kaXJlY3RpdmUiOiJmb250LXNyYyIsImVmZmVjdGl2ZS1kaXJlY3RpdmUiOiJmb250LXNyYyIsIm9yaWdpbmFsLXBvbGljeSI6ImRlZmF1bHQtc3JjIGh0dHBzOjsgcmVwb3J0LXVyaSBodHRwczovL2ZhYXMtc2ZvMy03ODcyYTFkZC5kb3NlcnZlcmxlc3MuY28vYXBpL3YxL3dlYi9mbi1lYjA3OTMwZC01MGQ1LTQ1YzktYWM1Yi1kZjA5OTg3YTIwMTcvZGVmYXVsdC90ZXN0aW5nIiwiZGlzcG9zaXRpb24iOiJyZXBvcnQiLCJibG9ja2VkLXVyaSI6ImRhdGEiLCJzdGF0dXMtY29kZSI6MCwic2NyaXB0LXNhbXBsZSI6IiJ9fQ==",
  "__ow_headers": {
    "accept": "*/*",
    "accept-encoding": "gzip",
    "accept-language": "en-US, en;q=0.9",
    "cdn-loop": "cloudflare",
    "cf-connecting-ip": "1.1.1.1",
    "cf-ipcountry": "US",
    "cf-ray": "74dcabd78bd4190e-EWR",
    "cf-visitor": "{\"scheme\":\"https\"}",
    "content-type": "application/csp-report",
    "dnt": "1",
    "host": "ccontroller",
    "origin": "https://live-blog.shellnetsecurity.com",
    "referer": "https://live-blog.shellnetsecurity.com/",
    "sec-ch-ua": "\"Google Chrome\";v=\"105\", \"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"105\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"macOS\"",
    "sec-fetch-dest": "report",
    "sec-fetch-mode": "no-cors",
    "sec-fetch-site": "cross-site",
    "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
    "x-forwarded-for": "1.1.1.1",
    "x-forwarded-proto": "https",
    "x-request-id": "2b1262be9ec3ec22e5c6e521ea13002b"
  },
  "__ow_path": "",
  "__ow_isBase64Encoded": true
}

There’s a bunch of detail in here but we can see that the important information is __ow_isBase64Encoded: true tells us that the details of importance are base64 encoded. The details we care about are in __ow_body. If we base64 decode this, we get the response from our client given our test policy of default-src https:

{
  "csp-report": {
    "document-uri": "about",
    "referrer": "",
    "violated-directive": "font-src",
    "effective-directive": "font-src",
    "original-policy": "default-src https:; report-uri https://faas-sfo3-7872a1dd.doserverless.co/api/v1/web/fn-eb07930d-50d5-45c9-ac5b-df09987a2017/default/testing",
    "disposition": "report",
    "blocked-uri": "data",
    "status-code": 0,
    "script-sample": ""
  }
}

Now we collect this data some more and use it to eventually test out an updated policy. Until the next post!