Writing

Software, technology, sysadmin war stories, and more. Feed
Monday, October 31, 2011

Content Security Policy with graceful downgrade

Yesterday, I ranted about the sorry state of affairs on the web and how the XSS situation is just ridiculous. I wished for an out-of-band solution where you could somehow convey content without having your privileges and/or authority attach to it.

Well, I'm glad I posted. My friend and all-around security hax0r Eric saw it and immediately pointed out something which had been in the works for a while called Content-Security-Policy, or CSP. It wouldn't give me quite the solution I was looking for, but it would take the bite out of code injection attacks.

Here's how it works. First, you figure out what you want to allow and what you want to deny. For my purposes, I'm okay with HTML5's video and audio tags working from anywhere, along with IMG SRC from anywhere. I only allow SCRIPT SRC from myself for my own site stuff and the Google CDN (free bandwidth!) for jQuery. Then I only allow CSS style directives from myself. That gives something like this:

allow 'self'; media-src *; img-src *; script-src 'self' https://ajax.googleapis.com; style-src 'self';

That may not be perfect. When in doubt, check the spec.

Now you need to serve it up with the page which will be doing the potentially risky inclusions. I opted for a .htaccess level tweak for the moment:

<FilesMatch index.html$>
Header set X-Content-Security-Policy "allow 'self'; media-src *; img-src *; script-src 'self' https://ajax.googleapis.com; style-src 'self';"
</FilesMatch>

If you aren't using Apache or don't have mod_header running, you'll need to find some other way to set this header.

Finally, your page needs to degrade gracefully. I do it with a few bits of Javascript to see if the browser is actually protecting me from badness. First, up in the header, I set up a variable:

var csp_status = "unknown";

I'm using jQuery, so later on I have a "document ready" function which runs when everything is in place. In it, I do a little script injection to see if it will run or not:

$(".status").html("<script>csp_status = \"inactive\";</script>");

"status" is just a div which is usually hidden and empty, so pushing this bit of scripting hackery into it changes nothing on my page. This is where the magic happens. If your browser is honoring CSP, it will treat that injection as a violation and will ignore it. Otherwise, your browser will run it and change csp_status from "unknown" to "inactive".

Finally, I run one more check after that to see what happened:

if (csp_status == "unknown") {
csp_status = "blocking";
}

At this point, you can now have one of three values in csp_status: unknown, inactive, or blocking. I deliberately chose three text values which were distinct so that the chances of someone mistaking one for another would be reduced. Consider what "inactive" vs. "active" would look like, for instance. They're too similar.

Likewise, I was originally thinking of having -1, 0, and 1. Then you have to remember which each one means! Forget that. When it comes to this kind of stuff, I want it to be stunningly simple and obvious.

So now here's the payoff: you can use this value to decide what to do later on. You can either inject the potentially harmful content or just opt out.

if (csp_status == "blocking") {
$(".post_content").html(post.content);
} else {
$(".post_content").html("Suppressed (non-CSP browser)");
}

And there it is. It isn't perfect, and it isn't wonderful, but it does manage to defang my biggest worry which is having evil scripts running on pages from my domain.

With this little obstacle out of my way, I've started feeling a lot better about working on my personal feed reader project. I even nuked my Google Reader subscriptions today. Now I have no reason to go back!