一、C#服务端接收数据的“隐形炸弹”——反序列化漏洞
很多做C#服务端开发的朋友,可能都有过这样的经历:为了省事,直接把客户端传过来的一串数据丢给系统自带的反序列化工具,想快速转成能直接用的对象。比如客户端发个JSON,服务端用JsonConvert.DeserializeObject<T>()转成实体类,整个过程看起来顺理成章,没毛病。但很少有人想过:如果客户端传的不是“正常数据”,而是专门造的恶意内容,会发生什么?
举个最直观的例子:假设服务端有个功能,接收客户端传的“用户配置”数据,用来更新用户的个性化设置,比如主题颜色、通知开关这些。如果开发时没对接收的内容做严格限制,客户端传的JSON里故意加了个能调用系统命令的字段,服务端反序列化时,会不会直接执行这个命令?答案是:会,而且很多人都踩过这个坑。
二、反序列化漏洞的核心原理:两个关键攻击点
要搞懂这个漏洞,得先拆解反序列化的本质:反序列化就是把“序列化后的字符串/字节”还原成内存里的对象的过程。这个过程有两个最危险的环节,刚好对应我们开头说的“未经校验的类型构造”和“属性注入”。
2.1 未经校验的类型构造:给了恶意对象“入场券”
很多人写代码时,会用“泛型”或者“动态类型”来接收数据,比如用object类型做反序列化的目标,或者允许客户端指定反序列化的类型。这时候就会出问题:正常情况下,服务端希望反序列化出的是UserConfig、Order这类业务实体,但如果客户端故意传一个恶意类型的信息,服务端没检查类型,就会把这个恶意类型的对象给造出来。
比如有个叫ProcessStartInfo的类型,是C#里专门用来启动系统进程的,比如启动计算器、打开某个文件、甚至执行恶意脚本。如果服务端允许反序列化这个类型,客户端传一个包含启动计算器命令的ProcessStartInfo对象,服务端一还原,计算器就直接弹出来了。
2.2 属性注入:给恶意对象“喂执行命令”
就算反序列化的类型是正常的业务类,也可能出问题。比如业务类里有个属性,是用来存“文件路径”的,开发时没限制这个属性的值只能是业务允许的路径,客户端传一个能执行命令的路径(比如Windows里的cmd.exe,或者Linux里的/bin/bash),服务端拿到这个对象后,不小心用这个属性的值去启动进程,就会触发恶意操作。
三、完整的漏洞演示:从接收数据到触发命令
我们来做一个完整的漏洞演示,所有代码都用C#,确保大家能复现。
首先,先搭一个简单的C#服务端,用ASP.NET Core Web API,功能是接收客户端传的JSON数据,反序列化后返回一个“操作结果”。
3.1 漏洞场景的服务端代码
// 定义一个业务实体类,用来接收客户端传的“操作参数”
public class OperationParam
{
// 用来存要执行的操作类型,比如“打开文件”
public string OperationType { get; set; }
// 用来存操作的目标路径,比如要打开的文件路径
public string TargetPath { get; set; }
}
// 定义API接口,接收POST请求
[ApiController]
[Route("[controller]")]
public class VulnerableController : ControllerBase
{
[HttpPost("DoOperation")]
public IActionResult DoOperation([FromBody] OperationParam param)
{
// 模拟业务逻辑:如果操作类型是“打开”,就启动对应的程序
if (param.OperationType == "Open")
{
// 这里直接用客户端传的TargetPath启动进程,完全没检查
System.Diagnostics.Process.Start(param.TargetPath);
}
return Ok("操作完成");
}
}
这个代码看起来很正常,甚至很多真实项目里都有类似的写法。现在我们来构造一个恶意的客户端请求:
3.2 恶意客户端请求的JSON
{
"OperationType": "Open",
"TargetPath": "calc.exe"
}
当服务端收到这个JSON,反序列化出OperationParam对象,然后启动calc.exe,计算器就弹出来了。这还是最温和的恶意操作,如果把TargetPath改成能执行删除文件、格式化磁盘、甚至远程控制的命令,后果不堪设想。
四、构建安全防线:白名单绑定+深度验证
既然漏洞这么危险,怎么防?核心就是我们开头说的两个方法:白名单绑定和深度验证。
4.1 白名单绑定:只允许反序列化安全的类型
白名单绑定的意思是:服务端反序列化时,只允许还原我们提前列好的、安全的类型,其他类型一概拒绝。怎么实现?不同的反序列化工具,有不同的配置方法,我们以常用的Newtonsoft.Json(Json.NET)和System.Text.Json(.NET Core自带的)为例。
4.1.1 Newtonsoft.Json的白名单配置
Newtonsoft.Json默认是允许反序列化所有类型的,所以我们要手动配置白名单:
// 定义一个反序列化的配置,只允许反序列化OperationParam类型
var jsonSettings = new JsonSerializerSettings
{
// 配置类型绑定,只允许OperationParam类型
TypeNameHandling = TypeNameHandling.None, // 禁止客户端指定类型
// 或者如果需要允许有限的类型,用下面的配置:
// SerializationBinder = new CustomSerializationBinder()
};
// 自定义序列化绑定器,只允许指定的类型
public class CustomSerializationBinder : ISerializationBinder
{
// 白名单类型列表
private readonly HashSet<Type> _allowedTypes = new HashSet<Type> { typeof(OperationParam) };
public void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
assemblyName = serializedType.Assembly.FullName;
typeName = serializedType.FullName;
}
public Type BindToType(string assemblyName, string typeName)
{
var type = Type.GetType($"{typeName}, {assemblyName}");
// 检查类型是否在白名单里
if (type != null && _allowedTypes.Contains(type))
{
return type;
}
// 不在白名单里,返回null,反序列化会失败
return null;
}
}
4.1.2 System.Text.Json的白名单配置
System.Text.Json默认的反序列化是安全的,因为它不会自动反序列化复杂的、可能有风险的类型,比如ProcessStartInfo,但我们还是要注意配置:
var jsonOptions = new JsonSerializerOptions
{
// 禁止反序列化非预期的类型
AllowTrailingCommas = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
4.2 深度验证:对反序列化后的对象做严格检查
就算反序列化的类型是安全的,我们也要对对象的每个属性做验证,确保值是合法的。比如之前的TargetPath属性,我们不能只看它是不是字符串,还要检查它是不是我们允许的路径,比如只能是/app/files/下面的路径,不能是系统路径,不能是可执行文件的路径。
深度验证的方法有很多,比如用C#的特性验证、自定义验证逻辑,或者用FluentValidation这类验证框架。我们以自定义验证为例:
// 改进后的OperationParam类,加入验证逻辑
public class OperationParam
{
public string OperationType { get; set; }
public string TargetPath { get; set; }
// 自定义验证方法
public (bool IsValid, string ErrorMessage) Validate()
{
// 1. 验证OperationType只能是允许的几个值
var allowedOperations = new List<string> { "Open", "View", "Download" };
if (!allowedOperations.Contains(OperationType))
{
return (false, "操作类型不合法");
}
// 2. 验证TargetPath只能是允许的路径
var allowedRootPath = Path.GetFullPath("/app/files/");
var targetPath = Path.GetFullPath(TargetPath);
// 检查目标路径是不是以允许的根路径开头
if (!targetPath.StartsWith(allowedRootPath, StringComparison.OrdinalIgnoreCase))
{
return (false, "目标路径不合法");
}
// 3. 验证TargetPath不能是可执行文件
var allowedExtensions = new List<string> { ".txt", ".jpg", ".pdf" };
var extension = Path.GetExtension(TargetPath).ToLower();
if (!allowedExtensions.Contains(extension))
{
return (false, "文件类型不合法");
}
return (true, "验证通过");
}
}
// 改进后的API接口,加入验证
[HttpPost("DoOperation")]
public IActionResult DoOperation([FromBody] OperationParam param)
{
var validateResult = param.Validate();
if (!validateResult.IsValid)
{
return BadRequest(validateResult.ErrorMessage);
}
if (param.OperationType == "Open")
{
System.Diagnostics.Process.Start(param.TargetPath);
}
return Ok("操作完成");
}
现在再传之前的恶意JSON,服务端会先验证TargetPath,发现不是允许的路径,直接返回错误,不会执行计算器。
五、漏洞的应用场景、优缺点和注意事项
5.1 应用场景
反序列化漏洞主要出现在需要接收外部数据的C#服务端场景,比如:
- Web API接收客户端传的JSON/XML数据;
- 消息队列(比如RabbitMQ、Kafka)接收其他服务传的序列化数据;
- 远程过程调用(RPC)服务接收调用方传的参数;
- 从数据库、缓存(比如Redis)读取序列化后的数据。
这些场景都有一个共同点:服务端接收的数据来自“不可信的外部”,如果没有防护,就可能被攻击。
5.2 技术优缺点
我们用来防护的两个核心技术,各有优缺点:
- 白名单绑定:
- 优点:从根源上杜绝了恶意类型的反序列化,防护力度强;
- 缺点:如果业务需要反序列化的类型很多,维护白名单会比较麻烦,而且如果配置不当(比如把危险类型不小心加到白名单里),会失效。
- 深度验证:
- 优点:灵活,能根据业务需求定制验证规则,就算反序列化出了恶意对象,也能在使用前拦住;
- 缺点:容易遗漏验证点,比如开发时觉得某个属性不会有问题,没做验证,结果被利用,而且验证逻辑要不断更新,新的业务场景可能需要加新的验证规则。
5.3 注意事项
除了白名单和深度验证,还有几个容易忽略的注意事项:
- 不要用
object做反序列化的目标类型:用object意味着允许反序列化任何类型,风险极高; - 不要信任任何外部数据:就算是来自内部服务的数据,也要做验证,因为内部服务也可能被攻击;
- 定期检查反序列化工具的版本:旧版本的反序列化工具可能有已知的漏洞,比如Newtonsoft.Json的旧版本就有反序列化漏洞;
- 避免使用危险的类型:如果业务不需要
ProcessStartInfo、FileStream这类可能有风险的类型,就不要在代码里出现,更不要允许反序列化。
六、文章总结
C#服务端的反序列化漏洞,本质上是“信任外部数据”导致的安全问题:开发时默认认为传过来的数据是合法的,没有做严格的限制和检查,给了恶意数据可乘之机。要防护这个漏洞,核心就是两个动作:第一,用白名单绑定,只允许反序列化我们自己认可的安全类型,把恶意类型挡在外面;第二,用深度验证,对反序列化后的每个属性做检查,确保值是符合业务规则的,就算恶意类型能进来,也没法触发危险操作。
很多开发者觉得安全是“安全团队的事”,自己只要实现业务逻辑就行,但实际上,大部分漏洞都是因为开发时的小疏忽导致的。多花几分钟做类型限制和属性验证,就能避免严重的安全事故,这是每个C#服务端开发者都应该养成的习惯。
评论
围绕“C#服务端接收不可信数据时的反序列化攻击面十分危险,未经校验的类型构造与属性注入可触发危险操作,依靠白名单绑定与深度验证构建防线”参与讨论