diff --git a/helpers/3p/object.js b/helpers/3p/object.js
index 796c1f32..a15001d6 100644
--- a/helpers/3p/object.js
+++ b/helpers/3p/object.js
@@ -2,6 +2,7 @@
var hasOwn = Object.hasOwnProperty;
var utils = require('./utils/');
+var common = require('../lib/common.js');
/**
* Expose `helpers`
@@ -209,7 +210,9 @@ helpers.JSONstringify = function(obj, indent) {
if (!utils.isNumber(indent)) {
indent = 0;
}
- return JSON.stringify(obj, null, indent);
+ // Escape characters that are unsafe when this output is embedded inside an
+ // HTML `.
+ return common.escapeJsonForHtml(JSON.stringify(obj, null, indent));
};
/**
diff --git a/helpers/json.js b/helpers/json.js
index e47ed019..9b05ac4a 100644
--- a/helpers/json.js
+++ b/helpers/json.js
@@ -4,7 +4,7 @@ const common = require('./lib/common.js');
const factory = globals => {
return function(data) {
data = common.unwrapIfSafeString(globals.handlebars, data);
- return JSON.stringify(data);
+ return common.escapeJsonForHtml(JSON.stringify(data));
};
};
diff --git a/helpers/lib/common.js b/helpers/lib/common.js
index 538d1249..5e56d491 100644
--- a/helpers/lib/common.js
+++ b/helpers/lib/common.js
@@ -11,7 +11,7 @@ function isValidURL(val) {
/*
* Based on https://github.com/jonschlinkert/get-value/blob/2.0.6/index.js with some enhancements.
- *
+ *
* - Performs "hasOwnProperty" checks for safety.
* - Now accepts Handlebars.SafeString paths.
*/
@@ -67,6 +67,38 @@ function unwrapIfSafeString(handlebars, val) {
return val;
}
+// Maps the char code of characters that are valid inside a JSON string but
+// dangerous when that JSON is emitted (unescaped) inside an HTML `; `>` guards
+// against `-->`/`]]>`; U+2028/U+2029 are illegal in JS string literals. Every
+// replacement is still valid JSON and round-trips through `JSON.parse`.
+const HTML_UNSAFE_JSON_ESCAPES = {
+ 0x3c: '\\u003c', // <
+ 0x3e: '\\u003e', // >
+ 0x2f: '\\u002f', // /
+ 0x2028: '\\u2028',
+ 0x2029: '\\u2029',
+};
+
+const HTML_UNSAFE_JSON_REGEX = /[<>/\u2028\u2029]/g;
+
+/**
+ * Escape the output of `JSON.stringify` so it can be safely embedded inside an
+ * HTML '});
+ expect(res).to.equal('"Music\\u003c\\u002fscript\\u003e\\u003cscript\\u003ealert(1)\\u003c\\u002fscript\\u003e"');
+ expect(res).to.not.contain('');
+ done();
+ });
});
});
\ No newline at end of file
diff --git a/spec/helpers/json.js b/spec/helpers/json.js
index 8703ff2e..924a2fe3 100644
--- a/spec/helpers/json.js
+++ b/spec/helpers/json.js
@@ -10,7 +10,8 @@ describe('json helper', function() {
image_with_2_qs: {
data: urlData_2_qs
},
- object: { a: 1, b: "hello" }
+ object: { a: 1, b: "hello" },
+ xss: 'Music'
};
const runTestCases = testRunner({context});
@@ -27,7 +28,7 @@ describe('json helper', function() {
runTestCases([
{
input: '{{{json (getImage image_with_2_qs)}}}',
- output: '"https://cdn.example.com/path/to/original/image.png?c=2&imbypass=on"',
+ output: '"https:\\u002f\\u002fcdn.example.com\\u002fpath\\u002fto\\u002foriginal\\u002fimage.png?c=2&imbypass=on"',
},
], done);
});
@@ -40,4 +41,13 @@ describe('json helper', function() {
},
], done);
});
+
+ it('should escape HTML-unsafe characters so output is safe inside a