Then they kept it running to this very day.
In September 2015 a Dropbox security engineer published four posts on getting a Content Security Policy into production — including the problem that nearly stopped them. The policy is still in their response headers today, and you can check it in ten seconds.
A 2015 engineering series is a story. A live response header is evidence, so here is the evidence first. This is what www.dropbox.com sends, alongside a second policy that starts default-src 'none'. The command underneath it is how you confirm that for yourself rather than taking our word for it:
content-security-policy: report-to csp-metaserver-dynamic; report-uri https://www.dropbox.com/csp_log?policy_name=metaserver-dynamic; script-src 'unsafe-eval' 'strict-dynamic' 'nonce-geJS2gGDCRq2aliITopu54yda80=' 'nonce-SmNs6E/ir+QwOsckvq1zK+Ga9Sg='
Four things in there are worth pulling out. There are two nonces, regenerated per response, so injected script cannot execute whatever domain it claims to come from. There is 'strict-dynamic', which tells modern browsers to ignore the host allowlist entirely and trust the nonce instead. There is a reporting endpoint, because a policy nobody is watching is a policy nobody is maintaining. And the companion policy opens default-src 'none' — deny everything, then permit deliberately, which is the strongest starting point CSP offers.
Check it yourself. One command, no account:
curl -sI -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/140.0.0.0" \
https://www.dropbox.com/ | grep -i content-security-policy
The user agent matters, and it is not a trick to make the numbers look better. Dropbox serves a different policy to clients it cannot identify as a modern browser — covered below, because it turns out to be one of the more interesting things about their setup. Fetch that URL without a user agent and you will get their allowlist policy, no nonce in sight, and conclude this page is wrong.
In September 2015, Devdatta Akhawe of Dropbox Security published four posts on their engineering blog about deploying CSP. Not an announcement, and not a case study written by a vendor — a working engineer explaining what broke and how they fixed it. He later became Head of Security at Figma.
The report noise problem, and the pipeline they built to survive it.
Getting to nonces without rewriting the application by hand.
Their fight with eval, including a jQuery patch they open-sourced.
What to do about the code you did not write.
Their own definition, from the first post, is still the clearest one-liner on the subject: “CSP is a declarative mechanism to whitelist content sources (such as sources for scripts, objects, images) in a web application.”
Not writing the policy. Not deploying it. Reading the reports that came back.
“At scale, one of the first lessons of deploying CSP is the sheer noise in the reports that make the default report mechanism unusable.”
The cause was not attacks on Dropbox. It was the browsers themselves — extensions injecting scripts into pages, and malware doing the same on compromised machines. Every one of those produced a violation report indistinguishable, at first glance, from a real attack.
So before the reports were worth reading, Dropbox built a three-stage filter:
Reports whose blocked URI used chrome://, file: or jar: could only have come from something running on the visitor's own machine.
Ad injectors and malicious domains — superfish.com, a typosquat of the Mozilla add-ons domain — plus CDNs Dropbox did not use at all.
Reports naming a violated directive that could never appear in their real policy, such as one containing http: or :443.
That is a meaningful amount of engineering, and it produced no features. It existed so that the reports coming back from a security control could be read by a human being. Every organisation deploying CSP at scale hits the same wall, and in 2015 there was nothing to buy that solved it.
The most common objection to CSP is that adopting it means rewriting the application. Dropbox is the counter-example, and the detail is worth having.
The nonce rollout alone ran report-only for nearly a month. Nothing was blocked while they learned what the policy would have broken.
Their server-side HTML generation inserted the nonce attribute into script tags automatically. The change lived in one place rather than across every template.
Deprecated onX= attributes became nonced script blocks that attached the same listener through addEventListener, preserving the original timing behaviour.
Injected after DOMContentLoaded, omitting 'unsafe-inline'. Browsers enforce every policy they are given, so this still blocked handlers injected through DOM APIs after load.
They also hit a Firefox bug that reported violations for code which had in fact executed, and recommended turning Firefox reporting off until the fix shipped in Firefox 43. Deploying a security control at scale looks like this: mostly unglamorous, occasionally somebody else's bug.
The result, in their words: “Between Chrome, Firefox, Safari, and Edge, a huge chunk of our users have a strong mitigation in place” — mitigation that holds even when an injection succeeds.
Something you only notice by fetching the headers twice. Dropbox does not serve one policy to everybody. They run several named ones and choose between them per request:
| Policy name | Script control | Served to |
|---|---|---|
metaserver-dynamic | Two nonces + 'strict-dynamic' | Browsers that can use a nonce |
metaserver-whitelist | Host allowlist | Clients that cannot be identified as one |
coop-dws | Cross-origin isolation | Alongside the above |
Each one reports to its own endpoint, tagged with policy_name, so violations arrive already attributed to the policy that produced them.
That is the same instinct as the 2015 series, still visible today: browsers that can enforce the strong thing get the strong thing, and everything else gets the best available fallback rather than nothing. It is also why the verification command above sends a user agent — ask anonymously and you are served the fallback.
Four things, before anyone else points them out.
'unsafe-eval'It does, and it weakens that part of the policy. The third post in their series is Dropbox documenting their own fight with exactly that, which is more candour than most organisations manage. It does not touch the nonce: injected script still cannot run without the per-response token.
Which is why the live header leads this page rather than closing it. The writing is from 2015; the policy is what they sent today.
They built their own pipeline and report to their own endpoint. Nothing here is an endorsement, and we are not going to dress it up as one.
Dropbox never suggested otherwise — the fourth post in the series adds privilege separation for third-party code on top of the policy. CSP is one control among several.
Read the four posts together and the shape of the work is clear. Writing the policy was the small part. What took the effort was everything around it: filtering the reports until they meant something, staging the rollout so nothing broke, keeping a second policy for clients that could not use the first, and going back when a dependency turned out to need eval.
Dropbox built all of that themselves because in 2015 there was nothing to buy that did it. That is the part which has changed.
The filtering pipeline Dropbox had to engineer — drop the extension noise, discard the impossible, keep what actually came from your pages — is not a project any more. It is the thing you are pointing a header at.
The policy is still yours. It sits in your response headers, enforced by every browser that visits you, and it keeps working whether or not you have a relationship with us. What we do is the part Dropbox had to build: collect the reports, filter the noise, turn what is left into a policy you can maintain, keep verified copies of the scripts that actually ran, and tell you when any of it changes.
Nothing routes through us. Nothing runs on your pages. See how that maps to PCI DSS 6.4.3 and 11.6.1 →
Set one header and filtered, readable reports arrive from your first visitor. No agent on your pages, nothing in your request path.