Twelve Days of ZAPmas - Day 12 - Testing a new Content-Security-Policy

Twelve Days of ZAPmas - Day 12 - Testing a new Content-Security-Policy
Mic Whitehorn
Author: Mic Whitehorn

What is the CSP?

The Content-Security-Policy (CSP) is a widely recommended control and is predominantly viewed as an anti cross-site scripting control. One of its core capabilities is restricting what remote resources your app can interact with, and in which contexts (e.g. loading an image in an <img> element versus making an XHR request). It’s set by providing the policy in the Content-Security-Policy response header when the document is sent to the client. For most modern frameworks, this is the response carrying the index.html. The browser then enforces the policy. I’ve linked some additional resources below, but for this post I’m assuming that you have familiarity with the CSP already.

More on the CSP:

Where does Zed Attack Proxy come in?

Adding a CSP to an existing application has the tendency to break functionality while you figure out how to dial it in. This leads to a lot of weak CSPs in production deployments, where the goal of having a CSP has technically been met, but the security benefit is questionable. In order to tighten up the control, it’s typically necessary to change the application so that directives can be removed from the CSP.  I’m going to walk through using ZAP to inject a Content-Security-Policy-Report-Only header in only your own browser, so that you can test a CSP without impacting other users or actually breaking functionality. This approach is essentially the same whether you’re trying to add a CSP to an app that doesn’t have one, or if you have an existing one, but you’re trying out a stricter set of directives.

Setting Up CSP Tuner

For this post, I wrote a little utility to capture CSP reports and view the data. If you already have a service in place for capturing CSP violations, feel free to use that and skip this section. For that matter, you can try this out without capturing the violations, so this step isn’t essential, it just provides more readable feedback. If you want to use mine, you can get it from here:

It’s a node app with fairly straightforward instructions for starting it up either locally (with node on your machine) or in a Docker container. Those installation instructions are in the readme.

Using Replacer to Add the Response Header

We used replacer on day 9, but as a reminder, Replacer Options can be accessed through the main Tools menu. Then you will want to add a rule, with the Add... button on the right side, which will open a dialog like the one below.

ZAP's Replacer dialog with the Match Type, Match String, and Replacement String set as described in the following text.

A few notes on what I've changed here: 1) The match type is Response Header instead of Request header. 2) My match string is the header name - Content-Security-Policy-Report-Only 3) My replacement string is set to:

default-src 'self'; report-uri http://localhost:3006/report;

I don’t want to stick this header in for any of the other tools, it’s really explicitly for my proxying, so the initiators are set accordingly.

Replacer's replacement rule initiators tab, with only "Proxy messages" checked.

And if I generate some traffic, I can see the header in the Response.

A captured response in ZAP, showing the Content-Security-Policy-Report-Only header has been set in accordance to the Replacer rule.

Generating some Violations

If you’re testing a real application, hopefully you don’t have a cross-site scripting flaw that you know of (you would fix it if you did, of course). Since my app is a target app, I’ll drop a link that performs script execution into this description field, where I know it’s not being sanitized.

The ticket description field in the Wayfarer target app, with an HTML link set to execute javascript.

Allowing that script to execute is a clear violation of the CSP that I’m injecting, since ‘unsafe-inline’ isn’t set as an allowed script-src. But as you can see below, the execution isn’t being prevented, Firefox is simply sending violation reports and allowing everything to proceed.

The Wayfarer app with the alert pop-up showing script execution for the injected element was not prevented.

Refine and Repeat

If I had used the protective Content-Security-Policy header instead of the Report-Only variation, it actually would have broken the app completely because the API on port 3001 wasn’t allowed as a connect-src (self is the client app, which is on port 3000 - a totally different origin as far as the CSP is concerned). Therefore the regular CSP header would have prevented the client app from being able to communicate with its API. The violation report is below.

A content-security-policy violation JSON payload, with a highlighted section showing the blocked URI was wayfarer.test on port 3001.

If you don’t already have a finely tuned CSP, I would expect to run into issues like this, where you need to add to the CSP to get into a working state. So once I identify that a piece of normal app functionality is generating violations, I’ll go back into ZAP and add the appropriate directive to allow that behavior, as shown below.

ZAP's replacer opened again to the same rule that was created earlier, with a highlighted portion of the Replacement String showing that the connect-src directive was added for the Wayfarer API on port 3001.

A few other notes

When I was troubleshooting to ensure that I got the header setup correctly, and to figure out what CSP violations were occurring, I also had the developer console open in the browser. Initially I had a syntax error in the CSP itself, and wasn’t seeing my reports in CSP Tuner.

The console in the browser's dev tools shows several messages indicating CSP Reports being sent.

CSP Tuner captures a bunch of data, it’s meant to make it easier to see what directives have been violated and what you need to do to accommodate them.

The CSP Tuner's table of violations, showing a relatively barebones app.

But it’s also not a fully baked tool yet. It’s not yet what I envisioned when I started it. So bear in mind that it’s a very barebones tool today.

Final Thoughts for the Series

I’ve always been forthcoming that I came to this series as someone who hadn’t really used ZAP. My experience is heavily in testing apps with Burp. I found ZAP to be an impressive tool in the space, and I definitely want to continue using it and learning to leverage its capabilities better. I hope the series was helpful to the newcomer trying to figure out how to make the tool do useful stuff. I found out about the Out-of-Band testing add-on (OAST) right at the end of the series, so I know that’s an area I want to try out and write about in the coming year. 

Special Thanks

We were fortunate to get a guest post from Simon Bennetts (Twitter / Mastodon), who knows ZAP better than anyone. Simon was able to shed some light on several of the areas where I hadn’t been able to find the right documentation or correct setting or approach to make ZAP do what I wanted. I could see from ZAP’s GitHub issues, StackExchange posts, and the ZAP User’s Group on Google Groups, that Simon is always quick to jump in and help. I think the application security community is extremely fortunate to have him. With that in mind, I want to thank Simon and the many other volunteers who have contributed time, energy, and passion to the ZAP project. ZAP is a great tool, and I have no doubt it’s going to continue to get better.

Join the professionally evil newsletter