CVE-2024-21907: Improper Handling of Exceptional Conditions (CWE-755)
value: Newtonsoft.Json before version 13.0.1 is affected by a mishandling of exceptional conditions vulnerability. Crafted data that is passed to the JsonConvert.DeserializeObject method may trigger a StackOverflow exception resulting in denial of service. Depending on the usage of the library, an unauthenticated and remote attacker may be able to cause the denial of service condition.

CWE description: The product does not handle or incorrectly handles an exceptional condition.
CVSS v3 base score: 7.5
Published: 2024-01-03T16:15Z
Repository: https://github.com/JamesNK/Newtonsoft.Json
Language: C#
Files changed: Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs, Src/Newtonsoft.Json/JsonReader.cs, Src/Newtonsoft.Json/JsonSerializer.cs, Src/Newtonsoft.Json/JsonSerializerSettings.cs
Commit date: 2021-01-31 15:57:06 +1300
Commit message: Change JsonReader and JsonSerializer default max depth to 128 (#2462)


Diff: diff --git a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs
index 2ffbd2a0..5e662920 100644
--- a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs
+++ b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs
@@ -7993,7 +7993,48 @@ public void NullableDoubleEmptyValue()
         {
             ExceptionAssert.Throws<JsonReaderException>(
                 () => JsonConvert.DeserializeObject<EmptyJsonValueTestClass>("{ A: \"\", B: 1, C: 123, D: 1.23, E: , F: null }"),
                 "Unexpected character encountered while parsing value: ,. Path 'E', line 1, position 36.");
         }
+
+        [Test]
+        public void SetMaxDepth_DepthExceeded()
+        {
+            JsonTextReader reader = new JsonTextReader(new StringReader("[[['text']]]"));
+            Assert.AreEqual(128, reader.MaxDepth);
+
+            JsonSerializerSettings settings = new JsonSerializerSettings();
+            Assert.AreEqual(128, settings.MaxDepth);
+            Assert.AreEqual(false, settings._maxDepthSet);
+
+            // Default should be the same
+            Assert.AreEqual(reader.MaxDepth, settings.MaxDepth);
+
+            settings.MaxDepth = 2;
+            Assert.AreEqual(2, settings.MaxDepth);
+            Assert.AreEqual(true, settings._maxDepthSet);
+
+            JsonSerializer serializer = JsonSerializer.Create(settings);
+            Assert.AreEqual(2, serializer.MaxDepth);
+
+            ExceptionAssert.Throws<JsonReaderException>(
+                () => serializer.Deserialize(reader),
+                "The reader's MaxDepth of 2 has been exceeded. Path '[0][0]', line 1, position 3.");
+        }
+
+        [Test]
+        public void SetMaxDepth_DepthNotExceeded()
+        {
+            JsonTextReader reader = new JsonTextReader(new StringReader("['text']"));
+            JsonSerializerSettings settings = new JsonSerializerSettings();
+
+            settings.MaxDepth = 2;
+
+            JsonSerializer serializer = JsonSerializer.Create(settings);
+            Assert.AreEqual(2, serializer.MaxDepth);
+
+            serializer.Deserialize(reader);
+
+            Assert.AreEqual(128, reader.MaxDepth);
+        }
     }
 }
diff --git a/Src/Newtonsoft.Json/JsonReader.cs b/Src/Newtonsoft.Json/JsonReader.cs
index 736ee0df..e9d920a7 100644
--- a/Src/Newtonsoft.Json/JsonReader.cs
+++ b/Src/Newtonsoft.Json/JsonReader.cs
@@ -225,10 +225,12 @@ public FloatParseHandling FloatParseHandling
             set => _dateFormatString = value;
         }
 
         /// <summary>
         /// Gets or sets the maximum depth allowed when reading JSON. Reading past this depth will throw a <see cref="JsonReaderException"/>.
+        /// A null value means there is no maximum. 
+        /// The default value is <c>128</c>.
         /// </summary>
         public int? MaxDepth
         {
             get => _maxDepth;
             set
@@ -325,10 +327,11 @@ protected JsonReader()
         {
             _currentState = State.Start;
             _dateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind;
             _dateParseHandling = DateParseHandling.DateTime;
             _floatParseHandling = FloatParseHandling.Double;
+            _maxDepth = 128;
 
             CloseInput = true;
         }
 
         private void Push(JsonContainerType value)
diff --git a/Src/Newtonsoft.Json/JsonSerializer.cs b/Src/Newtonsoft.Json/JsonSerializer.cs
index 241ec06d..c4e6b693 100644
--- a/Src/Newtonsoft.Json/JsonSerializer.cs
+++ b/Src/Newtonsoft.Json/JsonSerializer.cs
@@ -512,11 +512,11 @@ public virtual CultureInfo Culture
         }
 
         /// <summary>
         /// Gets or sets the maximum depth allowed when reading JSON. Reading past this depth will throw a <see cref="JsonReaderException"/>.
         /// A null value means there is no maximum.
-        /// The default value is <c>null</c>.
+        /// The default value is <c>128</c>.
         /// </summary>
         public virtual int? MaxDepth
         {
             get => _maxDepth;
             set
diff --git a/Src/Newtonsoft.Json/JsonSerializerSettings.cs b/Src/Newtonsoft.Json/JsonSerializerSettings.cs
index fa924c73..9c856314 100644
--- a/Src/Newtonsoft.Json/JsonSerializerSettings.cs
+++ b/Src/Newtonsoft.Json/JsonSerializerSettings.cs
@@ -59,10 +59,11 @@ public class JsonSerializerSettings
         internal const StringEscapeHandling DefaultStringEscapeHandling = StringEscapeHandling.Default;
         internal const TypeNameAssemblyFormatHandling DefaultTypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple;
         internal static readonly CultureInfo DefaultCulture;
         internal const bool DefaultCheckAdditionalContent = false;
         internal const string DefaultDateFormatString = @"yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK";
+        internal const int DefaultMaxDepth = 128;
 
         internal Formatting? _formatting;
         internal DateFormatHandling? _dateFormatHandling;
         internal DateTimeZoneHandling? _dateTimeZoneHandling;
         internal DateParseHandling? _dateParseHandling;
@@ -323,15 +324,15 @@ public string DateFormatString
         }
 
         /// <summary>
         /// Gets or sets the maximum depth allowed when reading JSON. Reading past this depth will throw a <see cref="JsonReaderException"/>.
         /// A null value means there is no maximum.
-        /// The default value is <c>null</c>.
+        /// The default value is <c>128</c>.
         /// </summary>
         public int? MaxDepth
         {
-            get => _maxDepth;
+            get => _maxDepthSet ? _maxDepth : DefaultMaxDepth;
             set
             {
                 if (value <= 0)
                 {
                     throw new ArgumentException("Value must be positive.", nameof(value));

Vulnerable code:         /// The default value is <c>null</c>.
        /// The default value is <c>null</c>.
            get => _maxDepth;
Fixed code: 
        [Test]
        public void SetMaxDepth_DepthExceeded()
        {
            JsonTextReader reader = new JsonTextReader(new StringReader("[[['text']]]"));
            Assert.AreEqual(128, reader.MaxDepth);

            JsonSerializerSettings settings = new JsonSerializerSettings();
            Assert.AreEqual(128, settings.MaxDepth);
            Assert.AreEqual(false, settings._maxDepthSet);

            // Default should be the same
            Assert.AreEqual(reader.MaxDepth, settings.MaxDepth);

            settings.MaxDepth = 2;
            Assert.AreEqual(2, settings.MaxDepth);
            Assert.AreEqual(true, settings._maxDepthSet);

            JsonSerializer serializer = JsonSerializer.Create(settings);
            Assert.AreEqual(2, serializer.MaxDepth);

            ExceptionAssert.Throws<JsonReaderException>(
                () => serializer.Deserialize(reader),
                "The reader's MaxDepth of 2 has been exceeded. Path '[0][0]', line 1, position 3.");
        }

        [Test]
        public void SetMaxDepth_DepthNotExceeded()
        {
            JsonTextReader reader = new JsonTextReader(new StringReader("['text']"));
            JsonSerializerSettings settings = new JsonSerializerSettings();

            settings.MaxDepth = 2;

            JsonSerializer serializer = JsonSerializer.Create(settings);
            Assert.AreEqual(2, serializer.MaxDepth);

            serializer.Deserialize(reader);

            Assert.AreEqual(128, reader.MaxDepth);
        }
        /// A null value means there is no maximum. 
        /// The default value is <c>128</c>.
            _maxDepth = 128;
        /// The default value is <c>128</c>.
        internal const int DefaultMaxDepth = 128;
        /// The default value is <c>128</c>.
            get => _maxDepthSet ? _maxDepth : DefaultMaxDepth;