Today x.com forbids inline event handlers outright.
In June 2013 Twitter published a post explaining why Content Security Policy was running everywhere except their front page: too much inline JavaScript, accumulated over years. That is the objection you have. This is what happened to it.
Start with the admission, because it is the reason this page is worth reading. Here is Twitter, in June 2013, on their own engineering blog:
“As you probably can tell, you don't see CSP on Twitter.com today. As one of the oldest projects, there is a wealth of inline JavaScript which is delaying our rollout for all of our public properties.”
A wealth of inline JavaScript, in the oldest project, holding up the flagship domain. If you have ever concluded your own application is too far gone for a Content Security Policy, that is your argument, made publicly by Twitter, thirteen years ago.
This is what x.com sends today, with the long source lists trimmed:
content-security-policy: default-src 'self'; script-src 'self' 'nonce-PMSH3m//DKWmh6kUNafHkA==' 'unsafe-inline' 'wasm-unsafe-eval' 'report-sample' https://*.twimg.com …; script-src-attr 'none' 'report-sample'; style-src 'self' 'unsafe-inline' https://*.twimg.com …
The directive to look at is script-src-attr 'none'. That is inline event handlers — onclick and its relatives — forbidden outright, at the browser, on every page. The precise thing that was delaying the rollout in 2013 is now banned by policy on the front page.
Above it sits a nonce, regenerated per response, so injected script cannot execute whatever origin it claims. 'report-sample' asks the browser to include a snippet of whatever it blocked in the violation report, which is the difference between knowing something was stopped and knowing what it was. And default-src 'self' is the floor underneath all of it.
Check it yourself. One command, no account, no special user agent:
curl -sI https://x.com/ | grep -i content-security-policy
The nonce will differ from the one above, because it is regenerated for every response. That is the point of it.
Published accounts of a Content Security Policy stopping a live attack are close to nonexistent. The 2013 post contains one.
Embedded in it is a May 2012 message from the security researcher Masato Kinugawa, reporting an XSS on mobile.twitter.com that allowed a javascript: link to be embedded. He notes that the bug was fixed, that in Firefox the execution had been blocked by CSP, and that it was the first time he had seen CSP react to a real XSS.
Three caveats, because this is the sort of detail that gets overstated. The original is in Japanese, so this page is reporting it rather than quoting it. It is one researcher's account of one incident, in one browser, in 2012. And it is a data point, not a hit rate — nobody should extrapolate from it.
It is also a good illustration of why these accounts are so rare. A control that works produces silence. Nothing breaks, no incident is opened, and nobody writes it up — the violation report is the only trace the attempt leaves behind.
Which is the whole argument for collecting them. An enforcing policy without reporting still protects your users; it just does it without ever telling you it did, or what it stopped, or that the same thing is being tried again this week.
Three lines from the post, worth reading in order.
Their assessment of what disallowing inline JavaScript achieves — inline handlers, javascript: links and <script> blocks in page content, all refused by the browser.
Written in 2013, a decade before proving what may execute on a payment page became a compliance requirement rather than a good idea.
Their framing, not ours. CSP is not a replacement for fixing the bug, and the post never suggests it is.
On what to do with the violations, they were equally plain: “You can use these reports to tune the policies over time and look for anomalous behavior (which might indicate a successful exploit).” Tuning and detection, from the same stream, in the first year of the technology.
The interesting part of Twitter's deployment is not technical. Faced with a codebase full of inline JavaScript, the move that unblocked it was a policy about how people write code:
“We decided to fully invest in the technology and doing so would require a moratorium on inline JavaScript and eventually inline styles. We did receive some pushback, but the support was overwhelming and coming from all directions and all levels.”
A moratorium stops the backlog growing while you work through it. Without one you are cleaning up inline script at roughly the rate new inline script is being written, which is how a migration runs for years and arrives nowhere.
Two things followed from it that are worth having:
They concluded that contextual encoding of dynamic content is an anti-pattern: “By removing all inline JavaScript and inline event handlers, we completely eliminated the need to encode for the JavaScript context.” Values go into attributes or element bodies, HTML-encoded, and external JavaScript reads them from there.
They open-sourced secure_headers for Rails and built equivalent support into the framework under their Scala projects, so teams inherited the header rather than each writing one. That library is now maintained at GitHub — the same place their engineer went.
Their own summary of the effort: “Applying CSP sitewide is an effort that involved every web development team in the company.” That is honest about the cost, and it is also why the rollout ran easiest-properties-first: Twitter Mobile, Ads Center, Translation Center, Twitter for Business and Tweetdeck all had a policy while the front page did not.
Nobody deploying CSP expects the reports to be the hard part. Twitter did not either:
“We were really surprised with what kinds of reports we were seeing. By examining the CSP reports we found foreign domains pointing at our hosts, potential indicators of infected browsers/computers, non-security-related bugs in site functionality, as well as how popular certain browser extensions are among our visitors.”
Read that list again. Only one item on it is the thing they deployed CSP to find. The rest is inventory, telemetry and broken links — genuinely useful, and none of it what you are looking for when an alert fires. Their next sentence is the one every team rediscovers: “Most plugins aren't CSP-friendly so this can generate a lot of noise.”
That complaint is not unique to them, and its consistency is the most useful thing in this research:
| Who | When | What they wrote down |
|---|---|---|
| 2013 | Extensions generate “a lot of noise”; reports full of things that are not attacks | |
| GitHub | 2013 | Extension and bookmarklet violations across Chrome, Safari and Firefox, making analysis nearly impossible |
| Dropbox | 2015 | “Sheer noise in the reports”; built a three-stage filter before they were readable |
Three companies, three separate deployments, the same conclusion each time: writing the policy is the easy half. Reading what comes back is the half that needs building. Dropbox wrote up their filter in detail →
Four things, before anyone else points them out.
Quoted at the top of this page rather than buried, because the gap between that admission and today's header is the entire argument. A page that hid it would be found out in one click.
'unsafe-inline'It sits alongside the nonce, and any browser that understands nonces ignores it entirely. It is a fallback for browsers too old to understand the modern keyword, not a hole — but it is in the header, so it is on the page.
One browser, 2012, described by a third party in another language. It is worth a section because published examples barely exist, not because one anecdote establishes a rate.
Nothing here is an endorsement, and we are not going to dress it up as one. The 2013 post is also explicit that CSP is a layer on top of existing controls rather than a substitute for them, and we agree with it.
“Our codebase is too old for this” is the most common reason a Content Security Policy never gets deployed, and it is usually true at the moment it is said. It was true at Twitter in 2013, on the largest property they had, and they wrote it down instead of quietly not shipping.
What they did next is the transferable part. They stopped the problem growing, put the header in the platform so teams inherited it rather than adopting it, shipped the properties that were ready, and left the hardest one until it was. The front page was last, not never.
Report-only mode is the modern version of that first step. Nothing is blocked, and every inline script, every handler and every third-party host on your pages reports itself from real browsers — so the inventory you would otherwise have to assemble by reading your own templates arrives on its own.
That is the part we run. You set a header, the reports come to us, and what arrives back is filtered, grouped and readable rather than the raw stream Twitter described being surprised by. See how CSP reporting works →
The policy stays yours. It sits in your response headers, enforced by every browser that visits, and it keeps working whether or not you have a relationship with us. Nothing routes through us and nothing runs on your pages.
Start in report-only and let real browsers tell you what your pages actually run. Nothing blocked, nothing to break.