Quellcode durchsuchen

JSON.parse optimizations, refactoring, DoS-attack prevention (#1485)

tomatosalat0 vor 2 Jahren
Ursprung
Commit
d372b34777
4 geänderte Dateien mit 553 neuen und 394 gelöschten Zeilen
  1. 226 0
      Jint.Tests/Runtime/JsonTests.cs
  2. 304 394
      Jint/Native/Json/JsonParser.cs
  3. 5 0
      Jint/Options.Extensions.cs
  4. 18 0
      Jint/Options.cs

+ 226 - 0
Jint.Tests/Runtime/JsonTests.cs

@@ -1,4 +1,6 @@
+using Jint.Native;
 using Jint.Native.Json;
+using Jint.Native.Object;
 using Jint.Runtime;
 
 namespace Jint.Tests.Runtime
@@ -55,6 +57,9 @@ namespace Jint.Tests.Runtime
         [InlineData("[1,\na]", "Unexpected token 'a' in JSON at position 4")] // multiline
         [InlineData("\x06", "Unexpected token '\x06' in JSON at position 0")] // control char
         [InlineData("{\"\\v\":1}", "Unexpected token 'v' in JSON at position 3")] // invalid escape sequence
+        [InlineData("[,]", "Unexpected token ',' in JSON at position 1")]
+        [InlineData("{\"key\": ()}", "Unexpected token '(' in JSON at position 8")]
+        [InlineData(".1", "Unexpected token '.' in JSON at position 0")]
         public void ShouldReportHelpfulSyntaxErrorForInvalidJson(string json, string expectedMessage)
         {
             var engine = new Engine();
@@ -84,5 +89,226 @@ namespace Jint.Tests.Runtime
 
             Assert.Equal(expectedJson, result);
         }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(1)]
+        [InlineData(10)]
+        [InlineData(100)]
+        public void ShouldParseArrayIndependentOfLengthInSameOrder(int numberOfElements)
+        {
+            string json = $"[{string.Join(",", Enumerable.Range(0, numberOfElements))}]";
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            JsValue result = parser.Parse(json);
+            JsArray array = Assert.IsType<JsArray>(result);
+            Assert.Equal((uint)numberOfElements, array.Length);
+            for (int i = 0; i < numberOfElements; i++)
+            {
+                Assert.Equal(i, (int)array[i].AsNumber());
+            }
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(1)]
+        [InlineData(10)]
+        [InlineData(20)]
+        public void ShouldParseStringIndependentOfLength(int stringLength)
+        {
+            string generatedString = string.Join("", Enumerable.Range(0, stringLength).Select(index => 'A' + index));
+            string json = $"\"{generatedString}\"";
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            string value = parser.Parse(json).AsString();
+            Assert.Equal(generatedString, value, StringComparer.Ordinal);
+        }
+
+        [Fact]
+        public void CanParsePrimitivesCorrectly()
+        {
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            Assert.Same(JsBoolean.True, parser.Parse("true"));
+            Assert.Same(JsBoolean.False, parser.Parse("false"));
+            Assert.Same(JsValue.Null, parser.Parse("null"));
+        }
+
+        [Fact]
+        public void CanParseNumbersWithAndWithoutSign()
+        {
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            Assert.Equal(-1, (int)parser.Parse("-1").AsNumber());
+            Assert.Equal(0, (int)parser.Parse("-0").AsNumber());
+            Assert.Equal(0, (int)parser.Parse("0").AsNumber());
+            Assert.Equal(1, (int)parser.Parse("1").AsNumber());
+        }
+
+        [Fact]
+        public void DoesPreservesNumberToMaxSafeInteger()
+        {
+            // see https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.max_safe_integer
+            long maxSafeInteger = 9007199254740991;
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            Assert.Equal(maxSafeInteger, (long)parser.Parse("9007199254740991").AsNumber());
+        }
+
+        [Fact]
+        public void DoesSupportFractionalNumbers()
+        {
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            Assert.Equal(0.1d, parser.Parse("0.1").AsNumber());
+            Assert.Equal(1.1d, parser.Parse("1.1").AsNumber());
+            Assert.Equal(-1.1d, parser.Parse("-1.1").AsNumber());
+        }
+
+        [Fact]
+        public void DoesSupportScientificNotation()
+        {
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            Assert.Equal(100d, parser.Parse("1E2").AsNumber());
+            Assert.Equal(0.01d, parser.Parse("1E-2").AsNumber());
+        }
+
+        [Fact]
+        public void ThrowsExceptionWhenDepthLimitReachedArrays()
+        {
+            string json = GenerateDeepNestedArray(65);
+
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            JavaScriptException ex = Assert.Throws<JavaScriptException>(() => parser.Parse(json));
+            Assert.Equal("Max. depth level of JSON reached at position 64", ex.Message);
+        }
+
+        [Fact]
+        public void ThrowsExceptionWhenDepthLimitReachedObjects()
+        {
+            string json = GenerateDeepNestedObject(65);
+
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            JavaScriptException ex = Assert.Throws<JavaScriptException>(() => parser.Parse(json));
+            Assert.Equal("Max. depth level of JSON reached at position 320", ex.Message);
+        }
+
+        [Fact]
+        public void CanParseMultipleNestedObjects()
+        {
+            string objectA = GenerateDeepNestedObject(63);
+            string objectB = GenerateDeepNestedObject(63);
+            string json = $"{{\"a\":{objectA},\"b\":{objectB} }}";
+
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            ObjectInstance parsed = parser.Parse(json).AsObject();
+            Assert.True(parsed["a"].IsObject());
+            Assert.True(parsed["b"].IsObject());
+        }
+
+        [Fact]
+        public void CanParseMultipleNestedArrays()
+        {
+            string arrayA = GenerateDeepNestedArray(63);
+            string arrayB = GenerateDeepNestedArray(63);
+            string json = $"{{\"a\":{arrayA},\"b\":{arrayB} }}";
+
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            ObjectInstance parsed = parser.Parse(json).AsObject();
+            Assert.True(parsed["a"].IsArray());
+            Assert.True(parsed["b"].IsArray());
+        }
+
+        [Fact]
+        public void ArrayAndObjectDepthNotCountedSeparately()
+        {
+            // individual depth is below the default limit, but combined
+            // a max. depth level is reached.
+            string innerArray = GenerateDeepNestedArray(40);
+            string json = GenerateDeepNestedObject(40, innerArray);
+
+            var engine = new Engine();
+            var parser = new JsonParser(engine);
+
+            JavaScriptException ex = Assert.Throws<JavaScriptException>(() => parser.Parse(json));
+            Assert.Equal("Max. depth level of JSON reached at position 224", ex.Message);
+        }
+
+        [Fact]
+        public void CustomMaxDepthOfZeroDisallowsObjects()
+        {
+            var engine = new Engine();
+            var parser = new JsonParser(engine, maxDepth: 0);
+
+            JavaScriptException ex = Assert.Throws<JavaScriptException>(() => parser.Parse("{}"));
+            Assert.Equal("Max. depth level of JSON reached at position 0", ex.Message);
+        }
+
+        [Fact]
+        public void CustomMaxDepthOfZeroDisallowsArrays()
+        {
+            var engine = new Engine();
+            var parser = new JsonParser(engine, maxDepth: 0);
+
+            JavaScriptException ex = Assert.Throws<JavaScriptException>(() => parser.Parse("[]"));
+            Assert.Equal("Max. depth level of JSON reached at position 0", ex.Message);
+        }
+
+        [Fact]
+        public void MaxDepthDoesNotInfluencePrimitiveValues()
+        {
+            var engine = new Engine();
+            var parser = new JsonParser(engine, maxDepth: 1);
+
+            ObjectInstance parsed = parser.Parse("{\"a\": 2, \"b\": true, \"c\": null, \"d\": \"test\"}").AsObject();
+            Assert.True(parsed["a"].IsNumber());
+            Assert.True(parsed["b"].IsBoolean());
+            Assert.True(parsed["c"].IsNull());
+            Assert.True(parsed["d"].IsString());
+        }
+
+        [Fact]
+        public void MaxDepthGetsUsedFromEngineOptionsConstraints()
+        {
+            var engine = new Engine(options => options.MaxJsonParseDepth(0));
+            var parser = new JsonParser(engine);
+
+            Assert.Throws<JavaScriptException>(() => parser.Parse("[]"));
+        }
+
+        private static string GenerateDeepNestedArray(int depth)
+        {
+            string arrayOpen = new string('[', depth);
+            string arrayClose = new string(']', depth);
+            return $"{arrayOpen}{arrayClose}";
+        }
+
+        private static string GenerateDeepNestedObject(int depth)
+        {
+            return GenerateDeepNestedObject(depth, mostInnerValue: "1");
+        }
+
+        private static string GenerateDeepNestedObject(int depth, string mostInnerValue)
+        {
+            string objectOpen = string.Concat(Enumerable.Repeat("{\"A\":", depth));
+            string objectClose = new string('}', depth);
+            return $"{objectOpen}{mostInnerValue}{objectClose}";
+        }
     }
 }

Datei-Diff unterdrückt, da er zu groß ist
+ 304 - 394
Jint/Native/Json/JsonParser.cs


+ 5 - 0
Jint/Options.Extensions.cs

@@ -214,6 +214,11 @@ namespace Jint
             return options;
         }
 
+        public static Options MaxJsonParseDepth(this Options options, int maxDepth)
+        {
+            options.Json.MaxParseDepth = maxDepth;
+            return options;
+        }
 
         public static Options SetReferencesResolver(this Options options, IReferenceResolver resolver)
         {

+ 18 - 0
Jint/Options.cs

@@ -91,6 +91,12 @@ namespace Jint
         /// </remarks>
         public bool StringCompilationAllowed { get; set; } = true;
 
+        /// <summary>
+        /// Options for the built-in JSON (de)serializer which
+        /// gets used using <c>JSON.parse</c> or <c>JSON.stringify</c>
+        /// </summary>
+        public JsonOptions Json { get; set; } = new();
+
         /// <summary>
         /// Called by the <see cref="Engine"/> instance that loads this <see cref="Options" />
         /// once it is loaded.
@@ -413,4 +419,16 @@ namespace Jint
         /// </summary>
         public IModuleLoader ModuleLoader { get; set; } = FailFastModuleLoader.Instance;
     }
+
+    /// <summary>
+    /// JSON.parse / JSON.stringify related customization
+    /// </summary>
+    public class JsonOptions
+    {
+        /// <summary>
+        /// The maximum depth allowed when parsing JSON files using "JSON.parse",
+        /// defaults to 64.
+        /// </summary>
+        public int MaxParseDepth { get; set; } = 64;
+    }
 }

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.