Optimising Twitter's CSP header

I'm sat on a train right now and between bursts of WiFi connectivity I'm testing some code to parse a CSP header. Whilst looking for big headers to test it on I came across Twitter who are certainly at the top of the list, but does it need to be as big as it is?


The CSP Header

Content Security Policy is a great security mechanism and is, in short, insanely powerful and flexible. Every single site should be deploying it and enabling reporting to get real-time feedback about your site. To do this you need to create your policy and deliver it, most likely, as a HTTP response header. The size of a policy isn't really a reflection of anything about a site but of course the policy has to be sent so the number of bytes it takes up does matter. Here's my policy as seen on Security Headers to get an idea.


Screenshot-2018-01-16-12.07.22


This is probably a fairly average CSP header and is a reflection of the 3rd party content that I load throughout my site. Now if we take that header and compare it to the one that Twitter serves, you can see a pretty big difference!


Screenshot-2018-01-16-12.06.00


That is a pretty darn big CSP, but again, it doesn't imply anything more than Twitter loads a lot of 3rd party content, which as a social network you can totally see the requirement for. Still though, I see something big like that and immediately wonder if it can be made any smaller. I did do some micro-optimising on my blog a couple of years back and Twitter got a mention for their CSP there too, the hacker in me just can't see things and accept they're as good as they can be!


Optimising the CSP

Let's take a look at that raw CSP shall we!

script-src https://connect.facebook.net https://cm.g.doubleclick.net https://ssl.google-analytics.com https://graph.facebook.com https://twitter.com 'nonce-36qhoTsfniYxjWrI2mg8zA==' 'unsafe-eval' https://*.twimg.com https://api.twitter.com https://analytics.twitter.com https://publish.twitter.com https://ton.twitter.com https://syndication.twitter.com https://www.google.com https://t.tellapart.com https://platform.twitter.com https://www.google-analytics.com blob: 'self'; frame-ancestors 'self'; font-src https://twitter.com https://*.twimg.com data: https://ton.twitter.com https://fonts.gstatic.com https://maxcdn.bootstrapcdn.com https://netdna.bootstrapcdn.com 'self'; media-src https://rmpdhdsnappytv-vh.akamaihd.net https://prod-video-eu-central-1.pscp.tv https://v.cdn.vine.co https://dwo3ckksxlb0v.cloudfront.net https://twitter.com https://amp.twimg.com https://smmdhdsnappytv-vh.akamaihd.net https://*.twimg.com https://prod-video-eu-west-1.pscp.tv https://rmmdhdsnappytv-vh.akamaihd.net https://clips-media-assets.twitch.tv https://prod-video-us-west-2.pscp.tv https://prod-video-us-west-1.pscp.tv https://prod-video-ap-northeast-1.pscp.tv https://smdhdsnappytv-vh.akamaihd.net https://ton.twitter.com https://rmdhdsnappytv-vh.akamaihd.net https://mmdhdsnappytv-vh.akamaihd.net https://smpdhdsnappytv-vh.akamaihd.net https://prod-video-sa-east-1.pscp.tv https://mdhdsnappytv-vh.akamaihd.net https://prod-video-ap-southeast-2.pscp.tv https://mtc.cdn.vine.co https://dev-video-us-west-2.pscp.tv https://prod-video-us-east-1.pscp.tv blob: 'self' https://prod-video-ap-southeast-1.pscp.tv https://mpdhdsnappytv-vh.akamaihd.net https://dev-video-eu-west-1.pscp.tv; connect-src https://rmpdhdsnappytv-vh.akamaihd.net https://prod-video-eu-central-1.pscp.tv https://graph.facebook.com https://*.giphy.com https://dwo3ckksxlb0v.cloudfront.net https://vmaprel.snappytv.com https://smmdhdsnappytv-vh.akamaihd.net https://*.twimg.com https://embed.pscp.tv https://api.twitter.com https://prod-video-eu-west-1.pscp.tv https://rmmdhdsnappytv-vh.akamaihd.net https://clips-media-assets.twitch.tv https://prod-video-us-west-2.pscp.tv https://pay.twitter.com https://prod-video-us-west-1.pscp.tv https://analytics.twitter.com https://vmap.snappytv.com https://*.twprobe.net https://prod-video-ap-northeast-1.pscp.tv https://smdhdsnappytv-vh.akamaihd.net https://syndication.twitter.com https://sentry.io https://rmdhdsnappytv-vh.akamaihd.net https://media.riffsy.com https://mmdhdsnappytv-vh.akamaihd.net https://embed.periscope.tv https://smpdhdsnappytv-vh.akamaihd.net https://prod-video-sa-east-1.pscp.tv https://vmapstage.snappytv.com https://upload.twitter.com https://proxsee.pscp.tv https://mdhdsnappytv-vh.akamaihd.net https://prod-video-ap-southeast-2.pscp.tv https://dev-video-us-west-2.pscp.tv https://prod-video-us-east-1.pscp.tv 'self' https://vmap.grabyo.com https://prod-video-ap-southeast-1.pscp.tv https://mpdhdsnappytv-vh.akamaihd.net https://dev-video-eu-west-1.pscp.tv; style-src https://fonts.googleapis.com https://twitter.com https://*.twimg.com https://translate.googleapis.com https://ton.twitter.com 'unsafe-inline' https://platform.twitter.com https://maxcdn.bootstrapcdn.com https://netdna.bootstrapcdn.com 'self'; object-src https://twitter.com https://pbs.twimg.com; default-src 'self' blob:; frame-src https://staticxx.facebook.com https://twitter.com https://*.twimg.com https://5415703.fls.doubleclick.net https://player.vimeo.com https://pay.twitter.com https://www.facebook.com https://ton.twitter.com https://syndication.twitter.com https://vine.co twitter: https://www.youtube.com https://platform.twitter.com https://upload.twitter.com https://s-static.ak.facebook.com https://4337974.fls.doubleclick.net https://8122179.fls.doubleclick.net 'self' https://donate.twitter.com; img-src https://prod-profile.pscp.tv https://graph.facebook.com https://prod-thumbnail.pscp.tv https://*.giphy.com https://twitter.com https://*.twimg.com https://ad.doubleclick.net data: https://clips-media-assets.twitch.tv https://lumiere-a.akamaihd.net https://fbcdn-profile-a.akamaihd.net https://www.facebook.com https://ton.twitter.com https://*.fbcdn.net https://syndication.twitter.com https://media.riffsy.com https://www.google.com https://prod-profile.periscope.tv https://stats.g.doubleclick.net https://platform.twitter.com https://api.mapbox.com https://www.google-analytics.com blob: https://prod-thumbnail-small.pscp.tv https://prod-thumbnail-small.periscope.tv 'self' https://prod-thumbnail.periscope.tv; report-uri https://twitter.com/i/csp_report?a=NVQWGYLXFVZXO2LGOQ%3D%3D%3D%3D%3D%3D&ro=false;

I know that Twitter do some UA sniffing so I will be using the same UA throughout these tests and that CSP weighs in at 4.628 Kb in size. The very first thing that jumped out at me about that CSP is that every source in each source list has a scheme specified, they all have https:// at the start. In modern clients this isn't required and actually adds no value to the policy at all. If you specify example.com in one of your source lists then the browser can only load content from that source using the scheme of the page that served the policy. That means if you're serving this policy from https://twitter.com and the browser gets example.com in the policy, the browser can only use https://example.com to load content. The only exception to this rule is if the page is loaded over http:// like http://twitter.com in which case the browser can use either http://example.com or https://example.com to load content. Scheme upgrades are allowed for obvious reasons but never a scheme downgrade. Given that Twitter has HSTS enabled and they're even HSTS Preloaded, which you can see on the preload site, no modern browser is ever going to load this site over the http:// scheme and Twitter will redirect the browser if it did. This means we can safely chop the scheme out of all of these sources and reduce the size of the policy.


script-src connect.facebook.net cm.g.doubleclick.net ssl.google-analytics.com graph.facebook.com twitter.com 'nonce-36qhoTsfniYxjWrI2mg8zA==' 'unsafe-eval' *.twimg.com api.twitter.com analytics.twitter.com publish.twitter.com ton.twitter.com syndication.twitter.com www.google.com t.tellapart.com platform.twitter.com www.google-analytics.com blob: 'self'; frame-ancestors 'self'; font-src twitter.com *.twimg.com data: ton.twitter.com fonts.gstatic.com maxcdn.bootstrapcdn.com netdna.bootstrapcdn.com 'self'; media-src rmpdhdsnappytv-vh.akamaihd.net prod-video-eu-central-1.pscp.tv v.cdn.vine.co dwo3ckksxlb0v.cloudfront.net twitter.com amp.twimg.com smmdhdsnappytv-vh.akamaihd.net *.twimg.com prod-video-eu-west-1.pscp.tv rmmdhdsnappytv-vh.akamaihd.net clips-media-assets.twitch.tv prod-video-us-west-2.pscp.tv prod-video-us-west-1.pscp.tv prod-video-ap-northeast-1.pscp.tv smdhdsnappytv-vh.akamaihd.net ton.twitter.com rmdhdsnappytv-vh.akamaihd.net mmdhdsnappytv-vh.akamaihd.net smpdhdsnappytv-vh.akamaihd.net prod-video-sa-east-1.pscp.tv mdhdsnappytv-vh.akamaihd.net prod-video-ap-southeast-2.pscp.tv mtc.cdn.vine.co dev-video-us-west-2.pscp.tv prod-video-us-east-1.pscp.tv blob: 'self' prod-video-ap-southeast-1.pscp.tv mpdhdsnappytv-vh.akamaihd.net dev-video-eu-west-1.pscp.tv; connect-src rmpdhdsnappytv-vh.akamaihd.net prod-video-eu-central-1.pscp.tv graph.facebook.com *.giphy.com dwo3ckksxlb0v.cloudfront.net vmaprel.snappytv.com smmdhdsnappytv-vh.akamaihd.net *.twimg.com embed.pscp.tv api.twitter.com prod-video-eu-west-1.pscp.tv rmmdhdsnappytv-vh.akamaihd.net clips-media-assets.twitch.tv prod-video-us-west-2.pscp.tv pay.twitter.com prod-video-us-west-1.pscp.tv analytics.twitter.com vmap.snappytv.com *.twprobe.net prod-video-ap-northeast-1.pscp.tv smdhdsnappytv-vh.akamaihd.net syndication.twitter.com sentry.io rmdhdsnappytv-vh.akamaihd.net media.riffsy.com mmdhdsnappytv-vh.akamaihd.net embed.periscope.tv smpdhdsnappytv-vh.akamaihd.net prod-video-sa-east-1.pscp.tv vmapstage.snappytv.com upload.twitter.com proxsee.pscp.tv mdhdsnappytv-vh.akamaihd.net prod-video-ap-southeast-2.pscp.tv dev-video-us-west-2.pscp.tv prod-video-us-east-1.pscp.tv 'self' vmap.grabyo.com prod-video-ap-southeast-1.pscp.tv mpdhdsnappytv-vh.akamaihd.net dev-video-eu-west-1.pscp.tv; style-src fonts.googleapis.com twitter.com *.twimg.com translate.googleapis.com ton.twitter.com 'unsafe-inline' platform.twitter.com maxcdn.bootstrapcdn.com netdna.bootstrapcdn.com 'self'; object-src twitter.com pbs.twimg.com; default-src 'self' blob:; frame-src staticxx.facebook.com twitter.com *.twimg.com 5415703.fls.doubleclick.net player.vimeo.com pay.twitter.com www.facebook.com ton.twitter.com syndication.twitter.com vine.co twitter: www.youtube.com platform.twitter.com upload.twitter.com s-static.ak.facebook.com 4337974.fls.doubleclick.net 8122179.fls.doubleclick.net 'self' donate.twitter.com; img-src prod-profile.pscp.tv graph.facebook.com prod-thumbnail.pscp.tv *.giphy.com twitter.com *.twimg.com ad.doubleclick.net data: clips-media-assets.twitch.tv lumiere-a.akamaihd.net fbcdn-profile-a.akamaihd.net www.facebook.com ton.twitter.com *.fbcdn.net syndication.twitter.com media.riffsy.com www.google.com prod-profile.periscope.tv stats.g.doubleclick.net platform.twitter.com api.mapbox.com www.google-analytics.com blob: prod-thumbnail-small.pscp.tv prod-thumbnail-small.periscope.tv 'self' prod-thumbnail.periscope.tv; report-uri https://twitter.com/i/csp_report?a=NVQWGYLXFVZXO2LGOQ%3D%3D%3D%3D%3D%3D&ro=false;

That's not bad going and has already taken a pretty massive chunk out of the policy taking us down to 3.508 KB, a 24.2% reduction in size! You could be pretty happy with that but there's more here that we can save. Looking through the policy there is some duplication within the source lists themselves and one or two places we can further optimise. Take these snippets from above:


script-src 'self' twitter.com
font-src 'self' twitter.com
media-src 'self' twitter.com
style-src 'self' twitter.com
frame-src 'self' twitter.com
img-src 'self' twitter.com

The self keyword in CSP instructs the browser that it's allowed to load resources of that type from the same origin that served the policy. This means if you're delivering this CSP from twitter.com then self keyword has us covered and the declaration of twitter.com as a source is redundant. There's also one other small change that we can make along the same lines.


object-src twitter.com

It'd be a lot more efficient to use the self keyword here, rather than the domain, as it's a lot smaller. Let's add these changes in and see where that gets us.


script-src connect.facebook.net cm.g.doubleclick.net ssl.google-analytics.com graph.facebook.com 'nonce-36qhoTsfniYxjWrI2mg8zA==' 'unsafe-eval' *.twimg.com api.twitter.com analytics.twitter.com publish.twitter.com ton.twitter.com syndication.twitter.com www.google.com t.tellapart.com platform.twitter.com www.google-analytics.com blob: 'self'; frame-ancestors 'self'; font-src *.twimg.com data: ton.twitter.com fonts.gstatic.com maxcdn.bootstrapcdn.com netdna.bootstrapcdn.com 'self'; media-src rmpdhdsnappytv-vh.akamaihd.net prod-video-eu-central-1.pscp.tv v.cdn.vine.co dwo3ckksxlb0v.cloudfront.net amp.twimg.com smmdhdsnappytv-vh.akamaihd.net *.twimg.com prod-video-eu-west-1.pscp.tv rmmdhdsnappytv-vh.akamaihd.net clips-media-assets.twitch.tv prod-video-us-west-2.pscp.tv prod-video-us-west-1.pscp.tv prod-video-ap-northeast-1.pscp.tv smdhdsnappytv-vh.akamaihd.net ton.twitter.com rmdhdsnappytv-vh.akamaihd.net mmdhdsnappytv-vh.akamaihd.net smpdhdsnappytv-vh.akamaihd.net prod-video-sa-east-1.pscp.tv mdhdsnappytv-vh.akamaihd.net prod-video-ap-southeast-2.pscp.tv mtc.cdn.vine.co dev-video-us-west-2.pscp.tv prod-video-us-east-1.pscp.tv blob: 'self' prod-video-ap-southeast-1.pscp.tv mpdhdsnappytv-vh.akamaihd.net dev-video-eu-west-1.pscp.tv; connect-src rmpdhdsnappytv-vh.akamaihd.net prod-video-eu-central-1.pscp.tv graph.facebook.com *.giphy.com dwo3ckksxlb0v.cloudfront.net vmaprel.snappytv.com smmdhdsnappytv-vh.akamaihd.net *.twimg.com embed.pscp.tv api.twitter.com prod-video-eu-west-1.pscp.tv rmmdhdsnappytv-vh.akamaihd.net clips-media-assets.twitch.tv prod-video-us-west-2.pscp.tv pay.twitter.com prod-video-us-west-1.pscp.tv analytics.twitter.com vmap.snappytv.com *.twprobe.net prod-video-ap-northeast-1.pscp.tv smdhdsnappytv-vh.akamaihd.net syndication.twitter.com sentry.io rmdhdsnappytv-vh.akamaihd.net media.riffsy.com mmdhdsnappytv-vh.akamaihd.net embed.periscope.tv smpdhdsnappytv-vh.akamaihd.net prod-video-sa-east-1.pscp.tv vmapstage.snappytv.com upload.twitter.com proxsee.pscp.tv mdhdsnappytv-vh.akamaihd.net prod-video-ap-southeast-2.pscp.tv dev-video-us-west-2.pscp.tv prod-video-us-east-1.pscp.tv 'self' vmap.grabyo.com prod-video-ap-southeast-1.pscp.tv mpdhdsnappytv-vh.akamaihd.net dev-video-eu-west-1.pscp.tv; style-src fonts.googleapis.com *.twimg.com translate.googleapis.com ton.twitter.com 'unsafe-inline' platform.twitter.com maxcdn.bootstrapcdn.com netdna.bootstrapcdn.com 'self'; object-src 'self' pbs.twimg.com; default-src 'self' blob:; frame-src staticxx.facebook.com *.twimg.com 5415703.fls.doubleclick.net player.vimeo.com pay.twitter.com www.facebook.com ton.twitter.com syndication.twitter.com vine.co twitter: www.youtube.com platform.twitter.com upload.twitter.com s-static.ak.facebook.com 4337974.fls.doubleclick.net 8122179.fls.doubleclick.net 'self' donate.twitter.com; img-src prod-profile.pscp.tv graph.facebook.com prod-thumbnail.pscp.tv *.giphy.com *.twimg.com ad.doubleclick.net data: clips-media-assets.twitch.tv lumiere-a.akamaihd.net fbcdn-profile-a.akamaihd.net www.facebook.com ton.twitter.com *.fbcdn.net syndication.twitter.com media.riffsy.com www.google.com prod-profile.periscope.tv stats.g.doubleclick.net platform.twitter.com api.mapbox.com www.google-analytics.com blob: prod-thumbnail-small.pscp.tv prod-thumbnail-small.periscope.tv 'self' prod-thumbnail.periscope.tv; report-uri https://twitter.com/i/csp_report?a=NVQWGYLXFVZXO2LGOQ%3D%3D%3D%3D%3D%3D&ro=false;

Not bad, it gets us down to 3.431 KB which is a 25.9% reduction from the original policy with no change in functionality! Still though, I think there are a couple of other tweaks we can make here. The default-src is specified in the policy yet most other directive that fall back to the default are also specified. The only ones left that would fall back are child-src and worker-src. If the child-src directive was being relied upon then it should be an equivalent value to the frame-src directive which it was replaced by in CSP 2 (but yes that does change in CSP 3) so that means only the worker-src could reliably be falling back here. Given that all of the other directives are specified there could be some optimisations to be made here but I don't want to suggest changes that I think will break things so we'll skip over this one and leave it as a possibility for someone with more knowledge of why Twitter need that directive. The last optimisation that we can make safely is then on the report-uri directive, probably my favourite directive!


report-uri https://twitter.com/i/csp_report?a=NVQWGYLXFVZXO2LGOQ%3D%3D%3D%3D%3D%3D&ro=false;

The report-uri directive specifies a URL where reports are sent but it can be resolved relative to the URL of the page that served it, meaning we don't need the scheme or the domain here. The other thing that we can drop is the &ro=false GET parameter. This is to let Twitter know whether the policy that was served was in report only mode (the 'ro') or enforce mode. As you'd expect the 'ro' value is set to false here as this is an enforced policy, but wouldn't 0 and 1 have been smaller values? Anyway, we don't need the 'ro' parameter at all because modern version of Chrome now indicate their disposition within the report itself so there are no need for GET params like this. This is something I raised a bug for a long time ago and it's nice to see it's finally here. That means we can now chop the report-uri directive down and because it's the last directive in the policy we don't need the trailing semi-colon either.


script-src connect.facebook.net cm.g.doubleclick.net ssl.google-analytics.com graph.facebook.com 'nonce-36qhoTsfniYxjWrI2mg8zA==' 'unsafe-eval' *.twimg.com api.twitter.com analytics.twitter.com publish.twitter.com ton.twitter.com syndication.twitter.com www.google.com t.tellapart.com platform.twitter.com www.google-analytics.com blob: 'self'; frame-ancestors 'self'; font-src *.twimg.com data: ton.twitter.com fonts.gstatic.com maxcdn.bootstrapcdn.com netdna.bootstrapcdn.com 'self'; media-src rmpdhdsnappytv-vh.akamaihd.net prod-video-eu-central-1.pscp.tv v.cdn.vine.co dwo3ckksxlb0v.cloudfront.net amp.twimg.com smmdhdsnappytv-vh.akamaihd.net *.twimg.com prod-video-eu-west-1.pscp.tv rmmdhdsnappytv-vh.akamaihd.net clips-media-assets.twitch.tv prod-video-us-west-2.pscp.tv prod-video-us-west-1.pscp.tv prod-video-ap-northeast-1.pscp.tv smdhdsnappytv-vh.akamaihd.net ton.twitter.com rmdhdsnappytv-vh.akamaihd.net mmdhdsnappytv-vh.akamaihd.net smpdhdsnappytv-vh.akamaihd.net prod-video-sa-east-1.pscp.tv mdhdsnappytv-vh.akamaihd.net prod-video-ap-southeast-2.pscp.tv mtc.cdn.vine.co dev-video-us-west-2.pscp.tv prod-video-us-east-1.pscp.tv blob: 'self' prod-video-ap-southeast-1.pscp.tv mpdhdsnappytv-vh.akamaihd.net dev-video-eu-west-1.pscp.tv; connect-src rmpdhdsnappytv-vh.akamaihd.net prod-video-eu-central-1.pscp.tv graph.facebook.com *.giphy.com dwo3ckksxlb0v.cloudfront.net vmaprel.snappytv.com smmdhdsnappytv-vh.akamaihd.net *.twimg.com embed.pscp.tv api.twitter.com prod-video-eu-west-1.pscp.tv rmmdhdsnappytv-vh.akamaihd.net clips-media-assets.twitch.tv prod-video-us-west-2.pscp.tv pay.twitter.com prod-video-us-west-1.pscp.tv analytics.twitter.com vmap.snappytv.com *.twprobe.net prod-video-ap-northeast-1.pscp.tv smdhdsnappytv-vh.akamaihd.net syndication.twitter.com sentry.io rmdhdsnappytv-vh.akamaihd.net media.riffsy.com mmdhdsnappytv-vh.akamaihd.net embed.periscope.tv smpdhdsnappytv-vh.akamaihd.net prod-video-sa-east-1.pscp.tv vmapstage.snappytv.com upload.twitter.com proxsee.pscp.tv mdhdsnappytv-vh.akamaihd.net prod-video-ap-southeast-2.pscp.tv dev-video-us-west-2.pscp.tv prod-video-us-east-1.pscp.tv 'self' vmap.grabyo.com prod-video-ap-southeast-1.pscp.tv mpdhdsnappytv-vh.akamaihd.net dev-video-eu-west-1.pscp.tv; style-src fonts.googleapis.com *.twimg.com translate.googleapis.com ton.twitter.com 'unsafe-inline' platform.twitter.com maxcdn.bootstrapcdn.com netdna.bootstrapcdn.com 'self'; object-src 'self' pbs.twimg.com; default-src 'self' blob:; frame-src staticxx.facebook.com *.twimg.com 5415703.fls.doubleclick.net player.vimeo.com pay.twitter.com www.facebook.com ton.twitter.com syndication.twitter.com vine.co twitter: www.youtube.com platform.twitter.com upload.twitter.com s-static.ak.facebook.com 4337974.fls.doubleclick.net 8122179.fls.doubleclick.net 'self' donate.twitter.com; img-src prod-profile.pscp.tv graph.facebook.com prod-thumbnail.pscp.tv *.giphy.com *.twimg.com ad.doubleclick.net data: clips-media-assets.twitch.tv lumiere-a.akamaihd.net fbcdn-profile-a.akamaihd.net www.facebook.com ton.twitter.com *.fbcdn.net syndication.twitter.com media.riffsy.com www.google.com prod-profile.periscope.tv stats.g.doubleclick.net platform.twitter.com api.mapbox.com www.google-analytics.com blob: prod-thumbnail-small.pscp.tv prod-thumbnail-small.periscope.tv 'self' prod-thumbnail.periscope.tv; report-uri /i/csp_report?a=NVQWGYLXFVZXO2LGOQ%3D%3D%3D%3D%3D%3D&

That's it! We're now down to 3.403 KB in size and a 26.5% reduction in the size of the CSP header itself, all with no impact on the functionality of the policy. That's not bad going and whilst you might not consider a saving of 1.225 KB to be that significant, if Twitter served this policy a mere 1 billion times then these changes would save them well over a terabyte of bandwidth quite comfortably. Not bad going at all!

Author image
About Scott Helme
United Kingdom Website
Security researcher, entrepreneur and international speaker who specialises in web technologies.