diff --git a/mozilla/browser/components/places/content/utils.js b/mozilla/browser/components/places/content/utils.js index b9af0cfb87b..aa47158cc03 100644 --- a/mozilla/browser/components/places/content/utils.js +++ b/mozilla/browser/components/places/content/utils.js @@ -46,6 +46,8 @@ var Ci = Components.interfaces; var Cc = Components.classes; var Cr = Components.results; +Components.utils.import("resource://gre/modules/JSON.jsm"); + const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; const DESCRIPTION_ANNO = "bookmarkProperties/description"; const POST_DATA_ANNO = "URIProperties/POSTData"; @@ -543,7 +545,7 @@ var PlacesUtils = { } return node; } - return this.toJSONString(gatherDataPlace(convertNode(aNode))); + return JSON.toString(gatherDataPlace(convertNode(aNode))); case this.TYPE_X_MOZ_URL: function gatherDataUrl(bNode) { @@ -756,7 +758,7 @@ var PlacesUtils = { case this.TYPE_X_MOZ_PLACE: case this.TYPE_X_MOZ_PLACE_SEPARATOR: case this.TYPE_X_MOZ_PLACE_CONTAINER: - nodes = this.parseJSON("[" + blob + "]"); + nodes = JSON.fromString("[" + blob + "]"); break; case this.TYPE_X_MOZ_URL: var parts = blob.split("\n"); @@ -1516,113 +1518,6 @@ var PlacesUtils = { } } return -1; - }, - - /** - * Converts a JavaScript object into a JSON string - * (see http://www.json.org/ for the full grammar). - * - * The inverse operation consists of eval("(" + JSON_string + ")"); - * and should be provably safe. - * - * @param aJSObject is the object to be converted - * @return the object's JSON representation - */ - toJSONString: function PU_toJSONString(aJSObject) { - // these characters have a special escape notation - const charMap = { "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f", - "\r": "\\r", '"': '\\"', "\\": "\\\\" }; - // we use a single string builder for efficiency reasons - var parts = []; - - // this recursive function walks through all objects and appends their - // JSON representation to the string builder - function jsonIfy(aObj) { - if (typeof aObj == "boolean") { - parts.push(aObj ? "true" : "false"); - } - else if (typeof aObj == "number" && isFinite(aObj)) { - // there is no representation for infinite numbers or for NaN! - parts.push(aObj.toString()); - } - else if (typeof aObj == "string") { - aObj = aObj.replace(/[\\"\x00-\x1F\u0080-\uFFFF]/g, function($0) { - // use the special escape notation if one exists, otherwise - // produce a general unicode escape sequence - return charMap[$0] || - "\\u" + ("0000" + $0.charCodeAt(0).toString(16)).slice(-4); - }); - parts.push('"' + aObj + '"'); - } - else if (aObj == null) { - parts.push("null"); - } - else if (aObj instanceof Array) { - parts.push("["); - for (var i = 0; i < aObj.length; i++) { - jsonIfy(aObj[i]); - parts.push(","); - } - if (parts[parts.length - 1] == ",") - parts.pop(); // drop the trailing colon - parts.push("]"); - } - else if (typeof aObj == "object") { - parts.push("{"); - for (var key in aObj) { - jsonIfy(key.toString()); - parts.push(":"); - jsonIfy(aObj[key]); - parts.push(","); - } - if (parts[parts.length - 1] == ",") - parts.pop(); // drop the trailing colon - parts.push("}"); - } - else { - throw new Error("No JSON representation for this object!"); - } - } // end of jsonIfy definition - jsonIfy(aJSObject); - - var newJSONString = parts.join(" "); - // sanity check - so that API consumers can just eval this string - if (/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test( - newJSONString.replace(/"(\\.|[^"\\])*"/g, "") - )) - throw new Error("JSON conversion failed unexpectedly!"); - - return newJSONString; - }, - - /** - * Converts a JSON string into a JavaScript object - * (see http://www.json.org/ for the full grammar). - * - * @param jsonText is the object to be converted - * @return a JS Object - */ - parseJSON: function parseJSON(jsonText) { - var m = { - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }; - - var EVAL_SANDBOX = new Components.utils.Sandbox("about:blank"); - - if (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/.test(jsonText. - replace(/\\./g, '@'). - replace(/"[^"\\\n\r]*"/g, ''))) { - var j = Components.utils.evalInSandbox(jsonText, EVAL_SANDBOX); - return j; - } - else - throw new SyntaxError('parseJSON'); } }; diff --git a/mozilla/browser/components/search/nsSearchSuggestions.js b/mozilla/browser/components/search/nsSearchSuggestions.js index 4b3e8d8cb92..8573c4f1a65 100644 --- a/mozilla/browser/components/search/nsSearchSuggestions.js +++ b/mozilla/browser/components/search/nsSearchSuggestions.js @@ -55,6 +55,7 @@ const HTTP_BAD_GATEWAY = 502; const HTTP_SERVICE_UNAVAILABLE = 503; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/JSON.jsm"); /** * SuggestAutoCompleteResult contains the results returned by the Suggest @@ -522,21 +523,7 @@ SuggestAutoComplete.prototype = { this._clearServerErrors(); - // This is a modified version of Crockford's JSON sanitizer, obtained - // from http://www.json.org/js.html. - // This should use built-in functions once bug 340987 is fixed. - const JSON_STRING = /^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/; - var sandbox = new Cu.Sandbox(this._suggestURI.prePath); - function parseJSON(aString) { - try { - if (JSON_STRING.test(aString)) - return Cu.evalInSandbox("(" + aString + ")", sandbox); - } catch (e) {} - - return []; - }; - - var serverResults = parseJSON(responseText); + var serverResults = JSON.fromString(responseText); var searchString = serverResults[0] || ""; var results = serverResults[1] || []; diff --git a/mozilla/browser/components/sessionstore/src/nsSessionStore.js b/mozilla/browser/components/sessionstore/src/nsSessionStore.js index c78dad4c436..4dd109db2c0 100644 --- a/mozilla/browser/components/sessionstore/src/nsSessionStore.js +++ b/mozilla/browser/components/sessionstore/src/nsSessionStore.js @@ -52,6 +52,7 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; +const Cu = Components.utils; const CID = Components.ID("{5280606b-2510-4fe0-97ef-9b5a22eafe6b}"); const CONTRACT_ID = "@mozilla.org/browser/sessionstore;1"; @@ -100,7 +101,10 @@ const CAPABILITIES = [ ]; // sandbox to evaluate JavaScript code from non-trustable sources -var EVAL_SANDBOX = new Components.utils.Sandbox("about:blank"); +var EVAL_SANDBOX = new Cu.Sandbox("about:blank"); + +// module for JSON conversion (needed for the nsISessionStore API) +Cu.import("resource://gre/modules/JSON.jsm"); function debug(aMsg) { aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); @@ -1618,7 +1622,7 @@ SessionStoreService.prototype = { try { cookieManager.add(cookie.host, cookie.path || "", cookie.name || "", cookie.value, !!cookie.secure, !!cookie.httponly, true, "expiry" in cookie ? cookie.expiry : MAX_EXPIRY); } - catch (ex) { Components.utils.reportError(ex); } // don't let a single cookie stop recovering + catch (ex) { Cu.reportError(ex); } // don't let a single cookie stop recovering } }, @@ -1909,12 +1913,12 @@ SessionStoreService.prototype = { * safe eval'ing */ _safeEval: function sss_safeEval(aStr) { - return Components.utils.evalInSandbox(aStr, EVAL_SANDBOX); + return Cu.evalInSandbox(aStr, EVAL_SANDBOX); }, /** * Converts a JavaScript object into a JSON string - * (see http://www.json.org/ for the full grammar). + * (see http://www.json.org/ for more information). * * The inverse operation consists of eval("(" + JSON_string + ")"); * and should be provably safe. @@ -1923,73 +1927,13 @@ SessionStoreService.prototype = { * @return the object's JSON representation */ _toJSONString: function sss_toJSONString(aJSObject) { - // these characters have a special escape notation - const charMap = { "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f", - "\r": "\\r", '"': '\\"', "\\": "\\\\" }; - // we use a single string builder for efficiency reasons - var parts = []; + var str = JSON.toString(aJSObject, ["_tab", "_hosts"] /* keys to drop */); - // this recursive function walks through all objects and appends their - // JSON representation to the string builder - function jsonIfy(aObj) { - if (typeof aObj == "boolean") { - parts.push(aObj ? "true" : "false"); - } - else if (typeof aObj == "number" && isFinite(aObj)) { - // there is no representation for infinite numbers or for NaN! - parts.push(aObj.toString()); - } - else if (typeof aObj == "string") { - aObj = aObj.replace(/[\\"\x00-\x1F\u0080-\uFFFF]/g, function($0) { - // use the special escape notation if one exists, otherwise - // produce a general unicode escape sequence - return charMap[$0] || - "\\u" + ("0000" + $0.charCodeAt(0).toString(16)).slice(-4); - }); - parts.push('"' + aObj + '"') - } - else if (aObj == null) { - parts.push("null"); - } - else if (aObj instanceof Array || aObj instanceof EVAL_SANDBOX.Array) { - parts.push("["); - for (var i = 0; i < aObj.length; i++) { - jsonIfy(aObj[i]); - parts.push(","); - } - if (parts[parts.length - 1] == ",") - parts.pop(); // drop the trailing colon - parts.push("]"); - } - else if (typeof aObj == "object") { - parts.push("{"); - for (var key in aObj) { - if (key == "_tab") - continue; // XXXzeniko we might even want to drop all private members - - jsonIfy(key.toString()); - parts.push(":"); - jsonIfy(aObj[key]); - parts.push(","); - } - if (parts[parts.length - 1] == ",") - parts.pop(); // drop the trailing colon - parts.push("}"); - } - else { - throw new Error("No JSON representation for this object!"); - } - } - jsonIfy(aJSObject); - - var newJSONString = parts.join(" "); // sanity check - so that API consumers can just eval this string - if (/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test( - newJSONString.replace(/"(\\.|[^"\\])*"/g, "") - )) + if (!JSON.isMostlyHarmless(str)) throw new Error("JSON conversion failed unexpectedly!"); - return newJSONString; + return str; }, /* ........ Storage API .............. */ diff --git a/mozilla/js/src/xpconnect/loader/JSON.jsm b/mozilla/js/src/xpconnect/loader/JSON.jsm new file mode 100644 index 00000000000..13b47a2b457 --- /dev/null +++ b/mozilla/js/src/xpconnect/loader/JSON.jsm @@ -0,0 +1,174 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla code. + * + * The Initial Developer of the Original Code is + * Simon Bünzli + * Portions created by the Initial Developer are Copyright (C) 2006-2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/** + * Utilities for JavaScript code to handle JSON content. + * See http://www.json.org/ for comprehensive information about JSON. + * + * Import this module through + * + * Components.utils.import("resource://gre/modules/JSON.jsm"); + * + * Usage: + * + * var newJSONString = JSON.toString( GIVEN_JAVASCRIPT_OBJECT ); + * var newJavaScriptObject = JSON.fromString( GIVEN_JSON_STRING ); + * + * Note: For your own safety, Objects/Arrays returned by + * JSON.fromString aren't instanceof Object/Array. + */ + +EXPORTED_SYMBOLS = ["JSON"]; + +// The following code is a loose adaption of Douglas Crockford's code +// from http://www.json.org/json.js (public domain'd) + +// Notable differences: +// * Unserializable values such as |undefined| or functions aren't +// silently dropped but always lead to a TypeError. +// * An optional key blacklist has been added to JSON.toString + +var JSON = { + /** + * Converts a JavaScript object into a JSON string. + * + * @param aJSObject is the object to be converted + * @param aKeysToDrop is an optional array of keys which will be + * ignored in all objects during the serialization + * @return the object's JSON representation + * + * Note: aJSObject MUST not contain cyclic references. + */ + toString: function JSON_toString(aJSObject, aKeysToDrop) { + // these characters have a special escape notation + const charMap = { "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f", + "\r": "\\r", '"': '\\"', "\\": "\\\\" }; + + // we use a single string builder for efficiency reasons + var pieces = []; + + // this recursive function walks through all objects and appends their + // JSON representation (in one or several pieces) to the string builder + function append_piece(aObj) { + if (typeof aObj == "boolean") { + pieces.push(aObj ? "true" : "false"); + } + else if (typeof aObj == "number" && isFinite(aObj)) { + // there is no representation for infinite numbers or for NaN! + pieces.push(aObj.toString()); + } + else if (typeof aObj == "string") { + aObj = aObj.replace(/[\\"\x00-\x1F\u0080-\uFFFF]/g, function($0) { + // use the special escape notation if one exists, otherwise + // produce a general unicode escape sequence + return charMap[$0] || + "\\u" + ("0000" + $0.charCodeAt(0).toString(16)).slice(-4); + }); + pieces.push('"' + aObj + '"') + } + else if (aObj === null) { + pieces.push("null"); + } + // if it looks like an array, treat it as such - this is required + // for all arrays from either outside this module or a sandbox + else if (aObj instanceof Array || + typeof aObj == "object" && "length" in aObj && + (aObj.length === 0 || aObj[aObj.length - 1] !== undefined)) { + pieces.push("["); + for (var i = 0; i < aObj.length; i++) { + append_piece(aObj[i]); + pieces.push(","); + } + if (pieces[pieces.length - 1] == ",") + pieces.pop(); // drop the trailing colon + pieces.push("]"); + } + else if (typeof aObj == "object") { + pieces.push("{"); + for (var key in aObj) { + // allow callers to pass objects containing private data which + // they don't want the JSON string to contain (so they don't + // have to manually pre-process the object) + if (aKeysToDrop && aKeysToDrop.indexOf(key) != -1) + continue; + + append_piece(key.toString()); + pieces.push(":"); + append_piece(aObj[key]); + pieces.push(","); + } + if (pieces[pieces.length - 1] == ",") + pieces.pop(); // drop the trailing colon + pieces.push("}"); + } + else { + throw new TypeError("No JSON representation for this object!"); + } + } + append_piece(aJSObject); + + return pieces.join(""); + }, + + /** + * Converts a JSON string into a JavaScript object. + * + * @param aJSONString is the string to be converted + * @return a JavaScript object for the given JSON representation + */ + fromString: function JSON_fromString(aJSONString) { + if (!this.isMostlyHarmless(aJSONString)) + throw new SyntaxError("No valid JSON string!"); + + var s = new Components.utils.Sandbox("about:blank"); + return Components.utils.evalInSandbox("(" + aJSONString + ")", s); + }, + + /** + * Checks whether the given string contains potentially harmful + * content which might be executed during its evaluation + * (no parser, thus not 100% safe! Best to use a Sandbox for evaluation) + * + * @param aString is the string to be tested + * @return a boolean + */ + isMostlyHarmless: function JSON_isMostlyHarmless(aString) { + const maybeHarmful = /[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/; + const jsonStrings = /"(\\.|[^"\\\n\r])*"/g; + + return !maybeHarmful.test(aString.replace(jsonStrings, "")); + } +}; diff --git a/mozilla/js/src/xpconnect/loader/Makefile.in b/mozilla/js/src/xpconnect/loader/Makefile.in index 9571a192ab2..8804e7e5fa9 100644 --- a/mozilla/js/src/xpconnect/loader/Makefile.in +++ b/mozilla/js/src/xpconnect/loader/Makefile.in @@ -56,7 +56,7 @@ REQUIRES = xpcom \ CPPSRCS = mozJSComponentLoader.cpp mozJSSubScriptLoader.cpp -EXTRA_JS_MODULES = XPCOMUtils.jsm +EXTRA_JS_MODULES = XPCOMUtils.jsm JSON.jsm include $(topsrcdir)/config/rules.mk diff --git a/mozilla/js/src/xpconnect/tests/unit/test_json.js b/mozilla/js/src/xpconnect/tests/unit/test_json.js new file mode 100644 index 00000000000..2a270233e54 --- /dev/null +++ b/mozilla/js/src/xpconnect/tests/unit/test_json.js @@ -0,0 +1,129 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Simon Bünzli + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +function run_test() { + // converts an object to a JSON string and tests its integrity + function toJSONString(a) { + var res = JSON.toString(a); + if (!JSON.isMostlyHarmless(res)) + throw new SyntaxError("Invalid JSON string: " + res); + return res; + } + + // ensures that an object can't be converted to a JSON string + function isInvalidType(a) { + try { + JSON.toString(a); + return false; + } catch (ex) { + return ex instanceof TypeError; + } + } + // ensures that a string can't be converted back to a JavaScript object + function isInvalidSyntax(a) { + try { + JSON.fromString(a); + return false; + } catch (ex) { + return ex instanceof SyntaxError; + } + } + + Components.utils.import("resource://gre/modules/JSON.jsm"); + do_check_eq(typeof(JSON), "object"); + + // some of the tests are adapted from /testing/mochitest/tests/test_Base.js + do_check_eq(toJSONString(true), "true"); + do_check_eq(toJSONString(false), "false"); + + do_check_eq(toJSONString(1), "1"); + do_check_eq(toJSONString(1.23), "1.23"); + do_check_eq(toJSONString(1.23e-45), "1.23e-45"); + + do_check_true(isInvalidType(Infinity)); + do_check_true(isInvalidType(NaN)); + + do_check_eq(toJSONString("Foo-Bar \b\t\n\f\r\"\\ \x01€"), + '"Foo-Bar \\b\\t\\n\\f\\r\\"\\\\ \\u0001\\u20ac"'); + + do_check_eq(toJSONString(null), "null"); + do_check_true(isInvalidType(undefined)); + + do_check_eq(toJSONString([1, "2", 3.3]), '[1,"2",3.3]'); + // duck-typed Array (since we'll never really get something instanceof Array) + do_check_eq(toJSONString({ 0: 0, 1: "1", 2: -2.2, length: 3 }), '[0,"1",-2.2]'); + + var obj = { a: 1, b: "2", c: [-3e+30] }; + do_check_eq(toJSONString(obj), '{"a":1,"b":"2","c":[-3e+30]}'); + do_check_eq(JSON.toString(obj, ["b", "c"] /* keys to drop */), '{"a":1}'); + + do_check_true(isInvalidType(function() { })); + + // make sure that toJSONString actually works... + do_check_eq(toJSONString(obj), JSON.toString(obj)); + + do_check_eq(JSON.fromString("true"), true); + do_check_eq(JSON.fromString("false"), false); + do_check_eq(JSON.fromString("1"), 1); + do_check_eq(JSON.fromString('"2.2"'), "2.2"); + do_check_eq(JSON.fromString("1.23e-45"), 1.23e-45); + do_check_true(isInvalidSyntax("NaN")); + do_check_eq(JSON.fromString('"Foo-Bar \\b\\t\\n\\f\\r\\"\\\\ \\u0001\\u20ac"'), + "Foo-Bar \b\t\n\f\r\"\\ \x01€"); + do_check_true(isInvalidSyntax('"multi\nline"')); + do_check_eq(JSON.fromString("null"), null); + do_check_true(isInvalidSyntax(".")); + + var res = JSON.fromString('[1,"2",3.3]'); + do_check_eq(res.length, 3); + do_check_eq(res[2], 3.3); + // res is an instance of the sandbox's array + do_check_false(res instanceof Array); + + res = JSON.fromString(toJSONString(obj)); + do_check_eq(res.a, obj.a); + do_check_eq(res.b, obj.b); + do_check_eq(res.c.length, obj.c.length); + do_check_eq(res.c[0], obj.c[0]); + + // those would throw on JSON.fromString if there's no object |a| + do_check_true(JSON.isMostlyHarmless("a")); + do_check_true(JSON.isMostlyHarmless("a[0]")); + do_check_true(JSON.isMostlyHarmless('a["alert(\\"P0wn3d!\\");"]')); + + do_check_false(JSON.isMostlyHarmless('(function() { alert("P0wn3d!"); })()')); + do_check_false(JSON.isMostlyHarmless('{ get a() { return "P0wn3d!"; } }')); +}