Deploying CSP: Dealing With Report Noise

Dropbox uses Content Security Policy (CSP) as a core defense against injection attacks. CSP lets a web application declare a whitelist of sources from which browsers may load content. A typical script-src directive from Dropbox's homepage policy lists every trusted URI (including full paths where supported) that might host script code. When a browser sees a script tag, it checks the src attribute against that list and blocks the request if the source isn't allowed, stopping HTML injection from executing.

script-src https://www.google.com/recaptcha/api/
https://ajax.googleapis.com/ajax/libs/jquery/  
https://cf.dropboxstatic.com/static/api/
https://cf.dropboxstatic.com/static/javascript/  
https://cf.dropboxstatic.com/static/coffee/compiled/
https://www.dropboxstatic.com/static/javascript/ 
https://www.dropboxstatic.com/static/coffee/ 
'unsafe-eval' 'self' 'unsafe-inline' 'nonce-w+NfAhVZiaXzHT1RmBqS'

CSP's value is clear, but rolling it out on a large, modern application is hard. The specification's report-only mode is meant to ease this transition. In report-only mode, a site can test a policy and receive violation reports at a chosen endpoint without actually blocking anything. The commonly recommended workflow is to iterate on a policy until the reports stop, then switch to enforcement. At Dropbox, however, we found that getting to "zero reports" was impossible in practice. The main culprit: browser extensions and third-party programs that inject scripts into pages or otherwise modify the DOM. Whatever they add is blocked by CSP, producing a steady stream of violations over which the website has no control.

We spent about a year refining a pipeline that filters out these false positives before anything is written to our analytics backend. The goal during this pre-rollout phase is not to catalog every injection attempt — the browser will block those once enforcement is on — but to uncover places where the policy itself is a breaking change. Filters let engineers focus on reports that indicate an actual misconfiguration in the whitelist.

Filtering on Scheme and Host

Our filtering process works in two stages. The first stage examines the URI scheme of the blocked resource:

_ignored_blockedURI_schemas = [
"http:",# We never use HTTP content so not a legit report
"mxaddon-pkg",# maxthon addon packages
"jar:",# firefox addons
"file:",#we never insert file URIs into the page
"tmtbff://",# ticketmaster toolbar?
"safari",# safari extensions
"chrome",# stuff like chromenull: chrome:// chrome-extension://
"webviewprogressproxy:",# added by browsers in webviews
"mbinit:",# MapBuilder
"symres:",# Norton
"resource",
];

Reports with a blocked-URI scheme starting with any entry in that list are ignored. The second stage checks the host component of the blocked URI:

_ignored_blockedURI_hosts=[
"tlscdn",
".superfish.com", 
"addons.mozilla.org", 
"v.zilionfast.in",
"widgets.amung.us",
"xls.searchfun.in",
"static.image2play.com",
"localhost",
"127.0.0.1",
"guzzlepraxiscommune",
"tfxiq",
"akamaihd.net", #Dropbox doesn't use Akamai CDN
"apollocdn",
"worldssl.net",
"shwcdn.net",
"cmptch.com",
"datafastguru.info",
"eshopcomp.com",
"hwcdn.net",
]

If the host contains any of those keywords, the report is dropped. Before applying this list, a site must verify that it doesn't legitimately rely on any of those domains — the filter assumes they are only injection sources controlled by third parties.

Additional Heuristics

We also saw noise from extensions that rewrote or appended to the CSP policy header itself before the browser processed it. To catch that case, the filter inspects the violated directive field. If that field includes http: or :443, the report is discarded, because our own policies never contain those strings.

One refinement we've considered but not yet needed is adding a hash of the current policy into the report-uri and accepting only reports whose policy field matches the hash. That would distinguish browser-reported violations from forged or modified ones, but the simpler filtering above has been sufficient so far.