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
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
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!