Google Code-in is an online programming competition for students hosted by Google that takes place every year.
When I was signing up for a second time, I put a payload into all the text fields. I didn’t expect anything to happen, but when I clicked the submit button, all the payloads were executed. And the payloads continued executing on every page I visited. This alone didn’t mean much as it would only classify as a self-XSS but meant that this didn’t have to be the only place the payload was improperly shown on the page. I submitted this bug to the support email and also to Google VRP in case it turns out to be a real issue.
In Google Code-in you can submit tasks for review and also can add comments to them. And as usual, I put the payload in the comment. Surprisingly, when I added the comment, the payload worked once again. And it stayed there even after I reloaded the page. I sent an update to Google and they fixed it the following day.
The Payload
Now let’s take a look at what happened with the payload.
They used script
elements with type application/json
generated on the backend to pass user data to the client-side.
<script type="application/json">
{"someData": true, "text": "hello world", "user": 123}
</script>
In the comment and other fields I used a simple payload like this:
"'><script src=x></script>{{1-1}}
When a new comment is sent, it’s also added to the JSON object which holds the comments of a task as well as some other data.
So when the comment was added, the JSON would look something like this:
<script type="application/json">
{
"someData": true,
"comments": [{
"id": 123,
"text": "\"'><script src=x></script>{{1-1}}"
}]
}
</script>
As you can see, the double quote is escaped correctly and it’s a perfectly valid JSON.
Except… they forgot to escape one important thing.
Parsing Context
As written in the HTML4 documentation:
The first occurrence of the character sequence “</” (end-tag open delimiter) is treated as terminating the end of the element’s content. In valid documents, this would be the end tag for the element.
This means as soon as the HTML parser sees </script>
, it assumes it is the end of that element.
We get even more info in the appendix of the documentation:
When script or style data is the content of an element (SCRIPT and STYLE), the data begins immediately after the element start tag and ends at the first ETAGO (“</”) delimiter followed by a name start character ([a-zA-Z]); note that this may not be the element’s end tag. Authors should therefore escape “</” within the content.
How to prevent this from happening, from the chapter Restrictions for contents of script elements:
The easiest and safest way to avoid the rather strange restrictions described in this section is to always escape “
<!--
” as “<\!--
“, “<script
” as “<\script
“, and “</script
” as “<\/script
” when these sequences appear in literals in scripts (e.g. in strings, regular expressions, or comments), and to avoid writing code that uses such constructs in expressions. Doing so avoids the pitfalls that the restrictions in this section are prone to triggering: namely, that, for historical reasons, parsing of script blocks in HTML is a strange and exotic practice that acts unintuitively in the face of these sequences.
This still wouldn’t be enough to get a working XSS on the page (in modern browsers) since they have a Content Security Policy set up. I wrote about bypassing CSP in a separate article. In a nutshell, CSP allows you to whitelist allowed sources of scripts, styles, and other resources to mitigate XSS attacks. This means a <script>
element just like that wouldn’t be able to get through CSP and therefore wouldn’t be executed.
Fortunately, Google Code-in uses AngularJS on its frontend. This means CSP can be easily bypassed. Expressions such as {{1-1}}
get easily evaluated (see Angular XSS on McDonalds.com). Since AngularJS 1.6, Google removed the expression sandbox completely, which means we can access the document with no problem just like this:
{{constructor.constructor('alert("xss")')()}}
Now we have a working payload that gets executed every time someone (in this case mentors or site admins) opens the comments page.
Timeline | |
---|---|
2018-10-30 | Vulnerability reported |
2018-10-31 | Fixed (by the dev team) |
2018-11-01 | Closed |
2018-11-21 | Reopened and accepted |
2018-11-21 | Priority changed to P2 |
2018-12-11 | Reward issued |
2018-12-12 | Marked as fixed |