From 60a6178131afec97b68c9a45bc24459f7b8bd905 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 4 Nov 2013 23:36:15 -0500 Subject: [PATCH] Fix #14492: More correct jQuery.parseJSON. Close gh-1419. --- src/ajax/parseJSON.js | 53 +++++++++++++++++------------- test/jquery.js | 3 +- test/unit/core.js | 75 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 102 insertions(+), 29 deletions(-) diff --git a/src/ajax/parseJSON.js b/src/ajax/parseJSON.js index 735015c9f..69b5c837d 100644 --- a/src/ajax/parseJSON.js +++ b/src/ajax/parseJSON.js @@ -2,39 +2,48 @@ define([ "../core" ], function( jQuery ) { -var rvalidchars = /^[\],:{}\s]*$/, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, - rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g; +var rvalidtokens = /(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g; jQuery.parseJSON = function( data ) { // Attempt to parse using the native JSON parser first if ( window.JSON && window.JSON.parse ) { - return window.JSON.parse( data ); + // Support: Android 2.3 + // Workaround failure to string-cast null input + return window.JSON.parse( data + "" ); } - if ( data === null ) { - return data; - } - - if ( typeof data === "string" ) { + var requireNonComma, + depth = null, + str = jQuery.trim( data + "" ); - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); + // Guard against invalid (and possibly dangerous) input by ensuring that nothing remains + // after removing valid tokens + return str && !jQuery.trim( str.replace( rvalidtokens, function( token, comma, open, close ) { - if ( data ) { - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test( data.replace( rvalidescape, "@" ) - .replace( rvalidtokens, "]" ) - .replace( rvalidbraces, "")) ) { + // Force termination if we see a misplaced comma + if ( requireNonComma && comma ) { + depth = 0; + } - return ( new Function( "return " + data ) )(); - } + // Perform no more replacements after returning to outermost depth + if ( depth === 0 ) { + return token; } - } - jQuery.error( "Invalid JSON: " + data ); + // Commas must not follow "[", "{", or "," + requireNonComma = open || comma; + + // Determine new depth + // array/object open ("[" or "{"): depth += true - false (increment) + // array/object close ("]" or "}"): depth += false - true (decrement) + // other cases ("," or primitive): depth += true - true (numeric cast) + depth += !close - !open; + + // Remove this token + return ""; + }) ) ? + ( Function( "return " + str ) )() : + jQuery.error( "Invalid JSON: " + data ); }; return jQuery.parseJSON; diff --git a/test/jquery.js b/test/jquery.js index bdee83c28..e94f55fe8 100644 --- a/test/jquery.js +++ b/test/jquery.js @@ -11,12 +11,13 @@ QUnit.config.urlConfig.push({ id: "basic", label: "Bypass optimizations", - tooltip: "Force use of the most basic code by disabling native querySelectorAll; contains; compareDocumentPosition" + tooltip: "Force use of the most basic code by disabling native querySelectorAll; contains; compareDocumentPosition; JSON.parse" }); if ( QUnit.urlParams.basic ) { document.querySelectorAll = null; document.documentElement.contains = null; document.documentElement.compareDocumentPosition = null; + window.JSON = null; } // iFrames won't load AMD (the iframe tests synchronously expect jQuery to be there) diff --git a/test/unit/core.js b/test/unit/core.js index 7bd126148..2af9f4a66 100644 --- a/test/unit/core.js +++ b/test/unit/core.js @@ -1341,13 +1341,35 @@ test("jQuery.parseHTML", function() { equal( jQuery.parseHTML("")[ 1 ].parentNode.nodeType, 11, "parentNode should be documentFragment" ); }); -test("jQuery.parseJSON", function(){ - expect( 9 ); +test("jQuery.parseJSON", function() { + expect( 20 ); + + strictEqual( jQuery.parseJSON( null ), null, "primitive null" ); + strictEqual( jQuery.parseJSON("0.88"), 0.88, "Number" ); + strictEqual( + jQuery.parseJSON("\" \\\" \\\\ \\/ \\b \\f \\n \\r \\t \\u007E \\u263a \""), + " \" \\ / \b \f \n \r \t ~ \u263A ", + "String escapes" + ); + deepEqual( jQuery.parseJSON("{}"), {}, "Empty object" ); + deepEqual( jQuery.parseJSON("{\"test\":1}"), { "test": 1 }, "Plain object" ); + deepEqual( jQuery.parseJSON("[0]"), [ 0 ], "Simple array" ); + + deepEqual( + jQuery.parseJSON("[ \"string\", -4.2, 2.7180e0, 3.14E-1, {}, [], true, false, null ]"), + [ "string", -4.2, 2.718, 0.314, {}, [], true, false, null ], + "Array of all data types" + ); + deepEqual( + jQuery.parseJSON( "{ \"string\": \"\", \"number\": 4.2e+1, \"object\": {}," + + "\"array\": [[]], \"boolean\": [ true, false ], \"null\": null }"), + { string: "", number: 42, object: {}, array: [[]], boolean: [ true, false ], "null": null }, + "Dictionary of all data types" + ); + + deepEqual( jQuery.parseJSON("\n{\"test\":1}\t"), { "test": 1 }, + "Leading and trailing whitespace are ignored" ); - equal( jQuery.parseJSON( null ), null, "Actual null returns null" ); - equal( jQuery.isEmptyObject( jQuery.parseJSON("{}") ), true, "Empty object returns empty object" ); - deepEqual( jQuery.parseJSON("{\"test\":1}"), { "test": 1 }, "Plain object parses" ); - deepEqual( jQuery.parseJSON("\n{\"test\":1}"), { "test": 1 }, "Leading whitespaces are ignored." ); raises(function() { jQuery.parseJSON(); }, null, "Undefined raises an error" ); @@ -1357,12 +1379,53 @@ test("jQuery.parseJSON", function(){ raises(function() { jQuery.parseJSON("''"); }, null, "Single-quoted string raises an error" ); + /* + + // Broken on IE8 + raises(function() { + jQuery.parseJSON("\" \\a \""); + }, null, "Invalid string escape raises an error" ); + + // Broken on IE8, Safari 5.1 Windows + raises(function() { + jQuery.parseJSON("\"\t\""); + }, null, "Unescaped control character raises an error" ); + + // Broken on IE8 + raises(function() { + jQuery.parseJSON(".123"); + }, null, "Number with no integer component raises an error" ); + + */ + raises(function() { + var result = jQuery.parseJSON("0101"); + + // Support: IE9+ + // Ensure base-10 interpretation on browsers that erroneously accept leading-zero numbers + if ( result === 101 ) { + throw new Error("close enough"); + } + }, null, "Leading-zero number raises an error or is parsed as decimal" ); raises(function() { jQuery.parseJSON("{a:1}"); }, null, "Unquoted property raises an error" ); raises(function() { jQuery.parseJSON("{'a':1}"); }, null, "Single-quoted property raises an error" ); + raises(function() { + jQuery.parseJSON("[,]"); + }, null, "Array element elision raises an error" ); + raises(function() { + jQuery.parseJSON("{},[]"); + }, null, "Comma expression raises an error" ); + raises(function() { + jQuery.parseJSON("[]\n,{}"); + }, null, "Newline-containing comma expression raises an error" ); + raises(function() { + jQuery.parseJSON("\"\"\n\"\""); + }, null, "Automatic semicolon insertion raises an error" ); + + strictEqual( jQuery.parseJSON([ 0 ]), 0, "Input cast to string" ); }); test("jQuery.parseXML", 8, function(){ -- 2.39.5