New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON escape rendered shoebox content #85

Merged
merged 1 commit into from Oct 14, 2016
Jump to file or symbol
Failed to load files and symbols.
+42 −15
Diff settings

Always

Just for now

JSON escape rendered shoebox content

Once rendering is completed, the contents of the shoebox is converted
into a string of JSON and inserted into the HTML output that is sent
back to the browser.

However, because JSON and HTML content are mixed, there is the potential
for security vulnerabilities. Specifically, if an attacker can cause an
application to place user-generated content into the shoebox, that
content could trick the browser into thinking JSON parsing had ended,
and evaluate arbitrary code in the origin of the host.

For example, if an untrusted user could supply an article with the
title of `</script><script>alert("owned")</script>`, the naive
interpolation of that into the shoebox might look like:

```html
<script type="fastboot/shoebox" id="shoebox-article">
{"article":{"title":"</script><script>alert("owned")</script>"}}
</script>
```

In this case, the browser would interpret the `</script>` inside the
JSON string as a real closing `script` tag, and thus would allow the
attacker's code to execute in the application's origin ("XSS").

Upon examining the HTML5 parser specification, [we can observe that there
is one, and only one, way to exit the "script data" state][spec]: the existence
of a `<` character, which moves the state machine into the "script data
less-than sign state". From the "script data less-than sign state",
there are several more states that can be traversed through, and it
requires the creation of a temporary buffer.

[spec]: https://www.w3.org/TR/html5/syntax.html#script-data-state

Thus we can conclude that the simplest, most effective way to prevent
inadvertent end-of-script situations is to prevent the `<` character
from ever appearing in shoebox content. If you never leave the "script
data" state, you can feel fairly certain that you have prevented this
particular vector of XSS attacks.

The good news is that this is easily accomplished. Both the JavaScript
specification and the JSON specification allow for [Unicode escape
sequences](https://mathiasbynens.be/notes/javascript-escapes#unicode).

Before insertion into the HTML document, we can replace characters that
could be ambiguous to the HTML parser and replace them with Unicode
escape sequences. These are no different from the unescaped values to
the eyes of the JSON or JavaScript parser, but give us a high degree of
confidence that the HTML parser will not attempt to treat them as
anything other than script data.

This commit Unicode escapes the following characters:

* `<` and `>`, to prevent ambiguity with opening and closing tags.
* `&`, to prevent ambiguity with HTML entities.
* `\u2028` and `\u2029`, Unicode line/paragraph separators, which the
  JSON parser and JavaScript parser treat differently and thus can lead
  to mismatched data if JavaScript is used as the JSON parser.
  • Loading branch information...
pwfisher authored and tomdale committed Aug 17, 2016
commit 08d6e0ad653723be2096a0fab326164bd8f63ebf
Copy path View file
@@ -310,7 +310,10 @@ function createShoebox(doc, fastbootInfo) {
if (!shoebox.hasOwnProperty(key)) { continue; }
let value = shoebox[key];
let scriptText = doc.createTextNode(JSON.stringify(value));
let textValue = JSON.stringify(value);
textValue = escapeJSONString(textValue);
let scriptText = doc.createRawHTMLSection(textValue);
let scriptEl = doc.createElement('script');
scriptEl.setAttribute('type', 'fastboot/shoebox');
@@ -322,6 +325,22 @@ function createShoebox(doc, fastbootInfo) {
return RSVP.resolve();
}
const JSON_ESCAPE = {
'&': '\\u0026',
'>': '\\u003e',
'<': '\\u003c',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
};
const JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/g;
function escapeJSONString(string) {
return string.replace(JSON_ESCAPE_REGEXP, function(match) {
return JSON_ESCAPE[match];
});
}
/*
* Builds a new FastBootInfo instance with the request and response and injects
* it into the application instance.
Copy path View file
@@ -10,7 +10,7 @@ const FastBoot = alchemistRequire('index');
describe("FastBootShoebox", function() {
it("can render the shoebox HTML", function() {
it("can render the escaped shoebox HTML", function() {
var fastboot = new FastBoot({
distPath: fixture('shoebox')
});
@@ -20,6 +20,11 @@ describe("FastBootShoebox", function() {
.then(html => {
expect(html).to.match(/<script type="fastboot\/shoebox" id="shoebox-key1">{"foo":"bar"}<\/script>/);
expect(html).to.match(/<script type="fastboot\/shoebox" id="shoebox-key2">{"zip":"zap"}<\/script>/);
// Special characters are JSON encoded, most notably the </script sequence.
expect(html).to.include('<script type="fastboot/shoebox" id="shoebox-key4">{"nastyScriptCase":"\\u003cscript\\u003ealert(\'owned\');\\u003c/script\\u003e\\u003c/script\\u003e\\u003c/script\\u003e"}</script>');
expect(html).to.include('<script type="fastboot/shoebox" id="shoebox-key5">{"otherUnicodeChars":"\\u0026\\u0026\\u003e\\u003e\\u003c\\u003c\\u2028\\u2028\\u2029\\u2029"}</script>');
});
});

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.