Research

Twitter said their own site was too old for CSP.

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.

2013
Deployed and documented
2012
One reported XSS, blocked
'none'
script-src-attr today
The Evidence

The job, finished

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:

x.com — HTTP response header
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:

Verify it yourself
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.

The Rare Bit

The XSS it actually caught

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.

The Reasoning

What they said it was for

Three lines from the post, worth reading in order.

On XSS

“Effectively eliminates all stored and reflected XSS”

Their assessment of what disallowing inline JavaScript achieves — inline handlers, javascript: links and <script> blocks in page content, all refused by the browser.

On integrity

“Documents the integrity of your web page inside a response header”

Written in 2013, a decade before proving what may execute on a payment page became a compliance requirement rather than a good idea.

On scope

An additional layer on top of existing controls

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 Method

The decision that made it work

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:

An entire class of escaping bug disappeared

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.

The policy lived in the platform, not in the pages

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.

The Pattern

What the reports showed them

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
Twitter2013Extensions generate “a lot of noise”; reports full of things that are not attacks
GitHub2013Extension and bookmarklet violations across Chrome, Safari and Firefox, making analysis nearly impossible
Dropbox2015“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 →

Being Straight

What this page doesn't claim

Four things, before anyone else points them out.

twitter.com had no policy at all in 2013

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.

Today's policy contains '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.

The blocked XSS is a single reported incident

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.

X is not a Report URI customer

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.

The Point

What this means for you

“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.

FAQ

Frequently asked questions

They did, in public, in June 2013: "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." CSP was live on Twitter Mobile, Ads Center, Translation Center, Twitter for Business and Tweetdeck at the time - the flagship was the hard one, and they said so.

An enforcing policy with default-src 'self', a nonce regenerated per response, 'report-sample' so violation reports carry a snippet of what was blocked, and script-src-attr 'none' - inline event handlers forbidden outright. That last directive bans the exact thing that delayed the 2013 rollout. The page carries the command to check that yourself.

Rarely visibly, which is the problem. The 2013 post embeds a May 2012 report from security researcher Masato Kinugawa of an XSS on mobile.twitter.com where, in Firefox, execution was blocked by the policy - he noted it was the first time he had seen CSP react to a real XSS. One incident, one browser, reported in Japanese by a third party. A working control produces silence, and the violation report is the only trace it leaves.

That is precisely Twitter's position in 2013, and they shipped anyway - easiest properties first, flagship last. What made it work was an organisational decision rather than a technical one: a moratorium on new inline JavaScript, agreed company-wide, so the problem stopped growing while they worked through the backlog.

It does, alongside the nonce. Any browser that understands nonces ignores unsafe-inline entirely when one is present, so it is a fallback for browsers too old to understand the modern keyword rather than a hole. Reading it as one is the most common way a modern policy gets misrepresented.

No, and we will not imply otherwise. The point of this page is the arc: a codebase everyone would call too old for CSP now enforces a nonce-based policy, and the reports were what made each step of that safe.

Old codebase? So was theirs.

Start in report-only and let real browsers tell you what your pages actually run. Nothing blocked, nothing to break.