They started with a wildcard and worked their way here.
Three posts across four years, from a permissive first policy in 2013 to a deny-by-default one today. They published what worked, and then published what it still couldn't stop.
This is what github.com sends. The long allowlists are trimmed; the two lines that matter are not:
content-security-policy: default-src 'none'; base-uri 'self'; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com; form-action 'self' github.com gist.github.com …; frame-ancestors 'none'; connect-src 'self' uploads.github.com api.github.com …
The whole of script-src is one hostname. Not 'self'. Not a nonce. No 'unsafe-inline', no 'unsafe-eval', no wildcard, no second source. On one of the largest applications on the web, exactly one origin is permitted to execute JavaScript, and it is not the origin serving the page.
Above it, default-src 'none' means every directive GitHub has not explicitly listed denies by default. That is the strongest baseline CSP offers, and it is the precise opposite of where they started.
Check it yourself. One command, no account, no special user agent:
curl -sI https://github.com/ | grep -i content-security-policy
In April 2013, GitHub shipped their first policy. It looked like this:
default-src *;
script-src 'self' assets-cdn.github.com jobs.github.com
ssl.google-analytics.com secure.gaug.es;
style-src 'self' assets-cdn.github.com 'unsafe-inline';
object-src 'self' assets-cdn.github.com;
default-src *. Everything permitted unless a more specific directive said otherwise, four sources allowed to run script including their own origin, and 'unsafe-inline' sitting in the styles. By the standard of the header at the top of this page it is barely a policy at all.
Here is GitHub's own assessment of it, written three years later:
“The policy was relatively simple, but substantially reduced the risk of XSS on GitHub.com.”
That is the most encouraging sentence in this research, and it is worth being blunt about why. A mediocre first policy was worth having. It was worth having immediately, it was worth having for three years while they improved it, and the improving is what got them to one hostname. The alternative — waiting until the policy is right before deploying it — produces nothing at all, for as long as it takes.
The 2013 work itself was mechanical rather than clever: the header went out through a Rails before-filter, inline configuration <script> tags became HTML data-* attributes, and inline event handlers were replaced by declarative data attributes read by a shared driver. The same shape of work Twitter and Dropbox describe, arrived at independently.
The single most instructive change in GitHub's policy is one most teams never consider: in 2016 they took 'self' out of script-src, leaving only the asset CDN.
Allowing your own origin to serve script feels obviously safe. It is your domain. Their reasoning for dropping it anyway:
“There may be a forgotten JSONP endpoint that doesn't sanitize the callback function name. Or, another endpoint that serves user-influenced content with a content-type that could be sniffed by browsers as JavaScript.”
An application the size of GitHub has endpoints nobody remembers, and any one of them that reflects attacker-controlled text into a response a browser is willing to treat as JavaScript becomes a script source the moment 'self' is in the directive. GitHub had several. Their answer was to serve every piece of raw, user-supplied content from a separate domain entirely, which then let them delete 'self' from the policy rather than trying to audit their way to safety.
The rest of the 2016 tightening followed the same instinct — narrow the thing rather than police it:
img-src went from * to a named listEvery third-party image source removed, which matters more than it sounds — an image request is a perfectly good way to send data somewhere.
form-action restricted, and it broke OAuthTheir OAuth flow redirected to third-party sites, which the directive refused. The fix was to stop using a 302 and hand the browser a meta refresh instead, decoupling form submission from navigation — later optimised to a JavaScript redirect with the meta refresh as fallback.
Browsers of the era checked only the first request in a redirect chain against the policy, so a redirect to a host that was not on the object-src list still executed. A browser bug, chained with a plugin, defeating a correctly written directive.
In January 2017 GitHub published a second post. Most of it is a catalogue of what their Content Security Policy could not stop.
They had commissioned Cure53 to answer a deliberately uncomfortable question: assuming an attacker gets content injection on GitHub.com, what can they still do? The findings were published rather than filed.
An unclosed tag swallows the rest of the page into an attribute, and the resulting request carries the token off to an attacker's host. No script execution required, so no script-src to enforce.
An attacker who cannot run JavaScript can still inject a form or manipulate existing markup, which is enough for a convincing phishing surface on a domain the user trusts.
Cure53 pointed them at <option>, <xmp> and <plaintext> as ways to capture page content. <plaintext> in Chrome they could not defend against.
Any of that is quotable by someone arguing CSP is not worth deploying, so here is the sentence they wrote to introduce it:
“With a solid foundation provided by CSP, we opened up an internal issue titled ‘Defending against post-CSP exploitation.’”
Post-CSP. The post is about what comes after a policy that works, not about one that failed. Script execution — the attack CSP exists to stop — was off the table, which is why the interesting question had become what was left. They then closed the remaining gaps with per-form CSRF tokens, scoped so narrowly that a token for starring one repository cannot be used to star another, and same-site cookies.
And they kept the policy. Nine years later it is stricter than almost anyone's, which is the conclusion worth drawing from all of this: a team that documents exactly where its control stops, and then keeps the control, is giving you the strongest possible endorsement of it. Anyone telling you their product has no limits is telling you something else.
Four things, before anyone else points them out.
script-src onlyconnect-src and child-src are long allowlists, not minimal ones. script-src is the directive that decides whether injected code runs, which is why it is the headline, but be precise about which claim is being made.
style-src carries 'unsafe-inline'The script side is exceptionally tight and the style side is not. The asymmetry is real and you would find it in the header anyway.
Single-trusted-host works because GitHub can serve all of its JavaScript from one CDN. An application that cannot wants nonces and 'strict-dynamic' instead. GitHub's route is not the only correct one.
Nothing here is an endorsement. And CSP is not sufficient on its own — their 2017 post is the proof of that, and this page agrees with them rather than arguing.
Read the three posts in order and the method is unmistakable. Ship something imperfect. Watch what it reports. Tighten one directive. Repeat, for years.
Every turn of that ratchet has the same prerequisite: knowing what your pages actually load. GitHub could only remove 'self' from script-src because they knew which endpoints served user-influenced content and where their scripts really came from. Guess wrong at that step and you take down your own application in production.
Report-only mode is what makes the ratchet safe. Tighten the directive, deploy it alongside the enforcing policy, and real browsers tell you exactly what the stricter version would have broken — before it breaks anything.
That is the loop we run. You set the headers, the reports come to us filtered and grouped rather than raw, and you get to see what the next turn would cost before you make it. See how CSP reporting works →
The policy stays yours, enforced by every browser that visits, working whether or not you have a relationship with us. Nothing routes through us and nothing runs on your pages.
One header, and real browser data on what your pages run — so every directive you narrow is a decision rather than a gamble.