找回密码
 立即注册
首页 业界区 业界 Newtonsoft.Json 与 System.Text.Json 多态反序列化的安 ...

Newtonsoft.Json 与 System.Text.Json 多态反序列化的安全性差异解析

慷规扣 昨天 14:45
  多态反序列化是处理继承结构对象序列化的常见需求,但不同 JSON 序列化库的实现机制差异会带来显著的安全风险。微软 CA2326 规则明确警示:避免使用非安全的 JsonSerializerSettings 配置(如 Newtonsoft.Json 的 TypeNameHandling 非 None 值),否则可能引发类型注入攻击。本文将对比 Newtonsoft.Json 与 System.Text.Json 在多态反序列化中的实现差异,重点分析安全性问题,并通过代码实例验证两者的安全表现。
多态反序列化的实现机制差异

Newtonsoft.Json:基于TypeNameHandling 的灵活设计

  Newtonsoft.Json 通过 TypeNameHandling 配置项控制是否在 JSON 中嵌入类型元数据。当设置 TypeNameHandling 支持多态时,JSON 会携带 $type 字段(包含类型的完全限定名和程序集信息),反序列化时直接根据该字段实例化对应类型。这种设计虽然灵活支持多态,但缺乏默认的类型校验机制,攻击者可构造包含恶意类型的 JSON,触发敏感类型实例化。
System.Text.Json:多态配置的安全设计

    System.Text.Json 默认不支持多态反序列化,需通过 [JsonDerivedType] 特性或 DerivedTypes 显式声明允许的派生类型。反序列化时仅处理配置过的类型,拒绝未授权的类型注入,从机制上规避了安全风险。
CA2326 规则的警示

1.webp

 
  CA2326 规则的核心是禁止使用 TypeNameHandling 非 None 值的配置 —— 攻击者可利用 $type 字段构造恶意 JSON,实例化如 ProcessStartInfo(执行系统命令)、FileStream(读写文件)等敏感类型,引发远程代码执行或数据泄露。
代码实例验证
  1. using Newtonsoft.Json;
  2. using Newtonsoft.Json.Serialization;
  3. using System.Diagnostics;
  4. using System.Text.Json;
  5. using System.Text.Json.Serialization;
  6. using System.Text.Json.Serialization.Metadata;
  7. namespace NewtonsoftSecurityDemo
  8. {
  9.     [JsonPolymorphic(TypeDiscriminatorPropertyName = "CustomerType")]
  10.     [JsonDerivedType(typeof(PaymentCompletedEvent), "PaymentCompletedEvent")]
  11.     [JsonDerivedType(typeof(OrderCreatedEvent), "OrderCreatedEvent")]
  12.     public class TransactionEvent
  13.     {
  14.         public string EventId { get; set; } = Guid.NewGuid().ToString();
  15.         public DateTime EventTime { get; set; } = DateTime.Now;
  16.         public string OrderId { get; set; }
  17.         // 业务扩展字段(攻击者利用的入口)
  18.         public object ExtData { get; set; }
  19.     }
  20.     public class PaymentCompletedEvent : TransactionEvent
  21.     {
  22.         public decimal Amount { get; set; }
  23.         public string PaymentMethod { get; set; }
  24.     }
  25.     public class OrderCreatedEvent : TransactionEvent
  26.     {
  27.         public string UserId { get; set; }
  28.         public int ItemCount { get; set; }
  29.     }
  30.     // Newtonsoft.Json 安全绑定器(演示白名单校验)
  31.     public class EventSerializationBinder : ISerializationBinder
  32.     {
  33.         // 仅允许的安全类型白名单
  34.         private readonly HashSet<string> _allowedTypes = new()
  35.         {
  36.             "NewtonsoftSecurityDemo.PaymentCompletedEvent",
  37.             "NewtonsoftSecurityDemo.OrderCreatedEvent",
  38.             //"System.Diagnostics.ProcessStartInfo"
  39.         };
  40.         public Type BindToType(string assemblyName, string typeName)
  41.         {
  42.             // 仅允许白名单内的类型
  43.             if (!_allowedTypes.Contains(typeName))
  44.             {
  45.                 throw new NotSupportedException($"禁止反序列化未授权类型:{typeName}");
  46.             }
  47.             return Type.GetType($"{typeName}, {assemblyName}") ?? typeof(TransactionEvent);
  48.         }
  49.         public void BindToName(Type serializedType, out string? assemblyName, out string? typeName)
  50.         {
  51.             assemblyName = serializedType.Assembly.FullName;
  52.             typeName = serializedType.FullName;
  53.         }
  54.     }
  55.     class Program
  56.     {
  57.         static void Main(string[] args)
  58.         {
  59.             Console.WriteLine("=== Newtonsoft.Json 命令执行攻击演示 ===");
  60.             Newtonsoft_Attack_ProcessStartInfo();
  61.             Console.WriteLine("\n=== Newtonsoft.Json 文件读取攻击演示 ===");
  62.             Newtonsoft_Attack_FileStream();
  63.             Console.WriteLine("\n=== Newtonsoft.Json 启用 SerializationBinder:安全防护演示 ===");
  64.             Newtonsoft_Secure_WithBinder();
  65.             Console.WriteLine("\n=== System.Text.Json 安全防护演示 ===");
  66.             SystemTextJson_Defense();
  67.             Console.ReadKey();
  68.         }
  69.         /// <summary>
  70.         /// 模拟:注入ProcessStartInfo执行系统命令
  71.         /// </summary>
  72.         static void Newtonsoft_Attack_ProcessStartInfo()
  73.         {
  74.             string maliciousCallbackJson = @$"
  75.                 {{
  76.                     ""$type"": ""NewtonsoftSecurityDemo.PaymentCompletedEvent, NewtonsoftSecurityDemo"",
  77.                     ""EventId"": ""{Guid.NewGuid()}"",
  78.                     ""OrderId"": ""ORD_{new Random().Next(1000, 9999)}"",
  79.                     ""Amount"": 999.00,
  80.                     ""PaymentMethod"": ""Alipay"",
  81.                     ""ExtData"": {{
  82.                         ""$type"": ""System.Diagnostics.ProcessStartInfo,System.Diagnostics.Process"",
  83.                         ""FileName"": ""cmd.exe"",
  84.                         ""Arguments"": ""/c echo 'some scripts' > C:\\temp\\attack_log.txt && echo 'doing' >> C:\\temp\\attack_log.txt"",
  85.                         ""UseShellExecute"": true
  86.                     }}
  87.                 }}";
  88.             var settings = new JsonSerializerSettings
  89.             {
  90.                 TypeNameHandling = TypeNameHandling.Auto,
  91.             };
  92.             var eventData = Newtonsoft.Json.JsonConvert.DeserializeObject<TransactionEvent>(maliciousCallbackJson, settings);
  93.             Console.WriteLine($"处理订单事件:{eventData.OrderId}");
  94.             if (eventData.ExtData is ProcessStartInfo psi)
  95.             {
  96.                 Directory.CreateDirectory("C:\\temp");
  97.                 Process.Start(psi);
  98.                 Console.WriteLine($"  [攻击成功] 执行命令:{psi.Arguments}");
  99.                 Console.WriteLine($"  [攻击结果] 生成文件:C:\\temp\\attack_log.txt 文件内容:");
  100.                 if (File.Exists("C:\\temp\\attack_log.txt"))
  101.                 {
  102.                     string content = File.ReadAllText("C:\\temp\\attack_log.txt");
  103.                     Console.WriteLine($"{content}");
  104.                 }
  105.             }
  106.         }
  107.         /// <summary>
  108.         /// 模拟:注入FileInfo读取敏感文件
  109.         /// </summary>
  110.         static void Newtonsoft_Attack_FileStream()
  111.         {
  112.             string targetFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "appsettings.json");
  113.             if (!File.Exists(targetFile))
  114.             {
  115.                 File.WriteAllText(targetFile, "ConnectionString: 123456");
  116.             }
  117.             string maliciousExportJson = @$"
  118.                 {{
  119.                     ""$type"": ""NewtonsoftSecurityDemo.OrderCreatedEvent, NewtonsoftSecurityDemo"",
  120.                     ""OrderId"": ""ORD_{new Random().Next(1000, 9999)}"",
  121.                     ""UserId"": ""user_{new Random().Next(100, 999)}"",
  122.                     ""ExtData"": {{
  123.                         ""$type"": ""System.IO.FileInfo"",
  124.                         ""FileName"": ""{targetFile.Replace("\", "\\\")}""
  125.                     }}
  126.                 }}";
  127.             var settings = new JsonSerializerSettings
  128.             {
  129.                 TypeNameHandling = TypeNameHandling.Auto
  130.             };
  131.             var eventData = Newtonsoft.Json.JsonConvert.DeserializeObject<TransactionEvent>(maliciousExportJson, settings);
  132.             Console.WriteLine($"处理订单导出:{eventData.OrderId}");
  133.             // 通过FileInfo读取文件内容(模拟攻击逻辑)
  134.             if (eventData.ExtData is FileInfo fileInfo)
  135.             {
  136.                 using (var sr = new StreamReader(fileInfo.OpenRead()))
  137.                 {
  138.                     string sensitiveContent = sr.ReadToEnd();
  139.                     Console.WriteLine($"  [攻击成功] 读取敏感文件内容:\n{sensitiveContent}");
  140.                 }
  141.             }
  142.         }
  143.         /// <summary>
  144.         /// Newtonsoft.Json 启用SerializationBinder:拦截恶意类型
  145.         /// </summary>
  146.         static void Newtonsoft_Secure_WithBinder()
  147.         {
  148.             string maliciousCallbackJson = @$"
  149.                 {{
  150.                     ""$type"": ""NewtonsoftSecurityDemo.PaymentCompletedEvent, NewtonsoftSecurityDemo"",
  151.                     ""EventId"": ""{Guid.NewGuid()}"",
  152.                     ""OrderId"": ""ORD_{new Random().Next(1000, 9999)}"",
  153.                     ""Amount"": 999.00,
  154.                     ""PaymentMethod"": ""Alipay"",
  155.                     ""ExtData"": {{
  156.                         ""$type"": ""System.Diagnostics.ProcessStartInfo,System.Diagnostics.Process"",
  157.                         ""FileName"": ""cmd.exe"",
  158.                         ""Arguments"": ""/c echo 'some scripts' > C:\\temp\\attack_log.txt && echo 'doing' >> C:\\temp\\attack_log.txt"",
  159.                         ""UseShellExecute"": true
  160.                     }}
  161.                 }}";
  162.             var settings = new JsonSerializerSettings
  163.             {
  164.                 TypeNameHandling = TypeNameHandling.Auto,
  165.                 SerializationBinder = new EventSerializationBinder() // 启用白名单校验
  166.             };
  167.             try
  168.             {
  169.                 var eventData = Newtonsoft.Json.JsonConvert.DeserializeObject<TransactionEvent>(maliciousCallbackJson, settings);
  170.                 if (eventData.ExtData is ProcessStartInfo)
  171.                 {
  172.                     Console.WriteLine("  [防护失效] 恶意类型未被拦截(异常)");
  173.                 }
  174.             }
  175.             catch (Exception ex)
  176.             {
  177.                 Console.WriteLine($"  [防护成功] 拦截未授权类型:{ex.Message}");
  178.             }
  179.         }
  180.         /// <summary>
  181.         /// System.Text.Json 安全防护验证
  182.         /// </summary>
  183.         static void SystemTextJson_Defense()
  184.         {
  185.             string maliciousCallbackJson = @$"
  186.                 {{
  187.                     ""CustomerType"": ""PaymentCompletedEvent"",
  188.                     ""EventId"": ""{Guid.NewGuid()}"",
  189.                     ""OrderId"": ""ORD_{new Random().Next(1000, 9999)}"",
  190.                     ""Amount"": 999.00,
  191.                     ""PaymentMethod"": ""Alipay"",
  192.                     ""ExtData"": {{
  193.                         ""$type"": ""System.Diagnostics.ProcessStartInfo,System.Diagnostics.Process"",
  194.                         ""FileName"": ""cmd.exe"",
  195.                         ""Arguments"": ""/c echo 'some scripts' > C:\\temp\\attack_log.txt && echo 'doing' >> C:\\temp\\attack_log.txt"",
  196.                         ""UseShellExecute"": true
  197.                     }}
  198.                 }}";
  199.             var eventData = System.Text.Json.JsonSerializer.Deserialize<TransactionEvent>(maliciousCallbackJson);
  200.             Console.WriteLine($"  主对象类型:{eventData.GetType().FullName}");
  201.             Console.WriteLine($"  ExtData 实际类型:{eventData.ExtData.GetType().FullName}");
  202.             if (eventData.ExtData is JsonElement)
  203.             {
  204.                 Console.WriteLine("  [防护成功] 恶意类型ProcessStartInfo被拦截,ExtData仅保留原始JSON结构,未反序列化为恶意对象");
  205.             }
  206.             else if (eventData.ExtData is ProcessStartInfo)
  207.             {
  208.                 Console.WriteLine("  [防护失效] 恶意类型解析成功");
  209.             }
  210.             else
  211.             {
  212.                 Console.WriteLine($"  [正常业务] 解析到合法类型:{eventData.ExtData.GetType().FullName}");
  213.             }
  214.             Console.WriteLine("\n尝试转换ExtData为ProcessStartInfo:");
  215.             try
  216.             {
  217.                 var psi = (ProcessStartInfo)eventData.ExtData;
  218.                 Console.WriteLine("  [防护失效] 恶意类型解析成功");
  219.             }
  220.             catch (InvalidCastException ex)
  221.             {
  222.                 Console.WriteLine($"  [防护成功] 强制转换失败,原因:{ex.Message}");
  223.             }
  224.         }
  225.     }
  226. }
复制代码
    运行结果为:
2.png

  通过 Demo 可以发现:
  - Newtonsoft 无防护时攻击成功;
  - Newtonsoft 启用 SerializationBinder 后拦截了恶意类型;
  - System.Text.Json 始终拦截恶意类型,ExtData 为 JsonElement,无法转换为 ProcessStartInfo。
为什么 Newtonsoft.Json 启用 SerializationBinder 可降低风险?

  先看代码:
  1. public class EventSerializationBinder : ISerializationBinder
  2. {
  3.     // 仅允许的安全类型白名单
  4.     private readonly HashSet<string> _allowedTypes = new()
  5.     {
  6.         "NewtonsoftSecurityDemo.PaymentCompletedEvent",
  7.         "NewtonsoftSecurityDemo.OrderCreatedEvent",
  8.         //"System.Diagnostics.ProcessStartInfo"
  9.     };
  10.     public Type BindToType(string assemblyName, string typeName)
  11.     {
  12.         // 仅允许白名单内的类型
  13.         if (!_allowedTypes.Contains(typeName))
  14.         {
  15.             throw new NotSupportedException($"禁止反序列化未授权类型:{typeName}");
  16.         }
  17.         return Type.GetType($"{typeName}, {assemblyName}") ?? typeof(TransactionEvent);
  18.     }
  19.     public void BindToName(Type serializedType, out string? assemblyName, out string? typeName)
  20.     {
  21.         assemblyName = serializedType.Assembly.FullName;
  22.         typeName = serializedType.FullName;
  23.     }
  24. }
复制代码
  SerializationBinder 的核心作用是:接管从 JSON 中的 $type 字符串 到实际 Type 类型的映射过程,强制校验类型合法性。简单说:
  - 无 SerializationBinder:反序列化器会无条件反射创建 $type 指定的任意类型,包括危险类型;
  - 有 SerializationBinder:反序列化器必须经过你的自定义校验逻辑,仅允许白名单内的类型被实例化,直接阻断恶意类型的创建。
小结

  Newtonsoft.Json 的 TypeNameHandling 机制虽灵活,但易被利用触发安全漏洞;System.Text.Json 通过显式多态配置白名单的设计,规避了类型注入风险。
  在实际开发中,针对多态场景,建议优先使用 System.Text.Json。若必须使用 Newtonsoft.Json,需遵循以下安全实践:
  - 避免使用 TypeNameHandling 非 None 值。
  - 若必须启用,需严格校验 $type 字段类型的合法性,仅允许安全类型。
 
  我希望您喜欢这篇文章,并一如既往地感谢您阅读并与朋友和同事分享我的文章。
3.png

 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册