Skip to content

Commit 08d6e0a

Browse files
pwfishertomdale
authored andcommitted
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.
1 parent 9899016 commit 08d6e0a

3 files changed

Lines changed: 42 additions & 15 deletions

File tree

src/ember-app.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,10 @@ function createShoebox(doc, fastbootInfo) {
310310
if (!shoebox.hasOwnProperty(key)) { continue; }
311311

312312
let value = shoebox[key];
313-
let scriptText = doc.createTextNode(JSON.stringify(value));
313+
let textValue = JSON.stringify(value);
314+
textValue = escapeJSONString(textValue);
315+
316+
let scriptText = doc.createRawHTMLSection(textValue);
314317
let scriptEl = doc.createElement('script');
315318

316319
scriptEl.setAttribute('type', 'fastboot/shoebox');
@@ -322,6 +325,22 @@ function createShoebox(doc, fastbootInfo) {
322325
return RSVP.resolve();
323326
}
324327

328+
const JSON_ESCAPE = {
329+
'&': '\\u0026',
330+
'>': '\\u003e',
331+
'<': '\\u003c',
332+
'\u2028': '\\u2028',
333+
'\u2029': '\\u2029'
334+
};
335+
336+
const JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/g;
337+
338+
function escapeJSONString(string) {
339+
return string.replace(JSON_ESCAPE_REGEXP, function(match) {
340+
return JSON_ESCAPE[match];
341+
});
342+
}
343+
325344
/*
326345
* Builds a new FastBootInfo instance with the request and response and injects
327346
* it into the application instance.

test/fastboot-shoebox-test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const FastBoot = alchemistRequire('index');
1010

1111
describe("FastBootShoebox", function() {
1212

13-
it("can render the shoebox HTML", function() {
13+
it("can render the escaped shoebox HTML", function() {
1414
var fastboot = new FastBoot({
1515
distPath: fixture('shoebox')
1616
});
@@ -20,6 +20,11 @@ describe("FastBootShoebox", function() {
2020
.then(html => {
2121
expect(html).to.match(/<script type="fastboot\/shoebox" id="shoebox-key1">{"foo":"bar"}<\/script>/);
2222
expect(html).to.match(/<script type="fastboot\/shoebox" id="shoebox-key2">{"zip":"zap"}<\/script>/);
23+
24+
// Special characters are JSON encoded, most notably the </script sequence.
25+
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>');
26+
27+
expect(html).to.include('<script type="fastboot/shoebox" id="shoebox-key5">{"otherUnicodeChars":"\\u0026\\u0026\\u003e\\u003e\\u003c\\u003c\\u2028\\u2028\\u2029\\u2029"}</script>');
2328
});
2429
});
2530

test/fixtures/shoebox/fastboot/fastboot-test.js

Lines changed: 16 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)