注意
BinaryFormatter 不建议支持。 仅将其用作无法立即迁移到新类型安全 API 的旧应用程序的临时迁移桥。 此方法具有重大安全风险。
本文介绍如何在 .NET 10 中配置对 Windows 窗体剪贴板操作的有限 BinaryFormatter 支持。
BinaryFormatter 已因安全漏洞从 .NET 9 的运行时中移除,但可以通过显式配置还原部分功能,以便需要时间迁移的旧版应用程序使用。
有关新类型安全 API 的完整迁移指南,请参阅 .NET 10 中的 Windows 窗体剪贴板和 DataObject 更改。
重要
除非另有说明,否则此内容仅适用于新式 .NET, 不适用于 .NET Framework。
先决条件
在继续之前,请查看以下概念:
- 当前应用程序如何在剪贴板操作中使用
BinaryFormatter。 - 导致删除的
BinaryFormatter安全漏洞。 - 迁移到新的类型安全剪贴板 API 的时间表。
有关详细信息,请参阅以下文章:
安全警告和风险
BinaryFormatter 出于以下原因,本质上不安全且已弃用:
- 任意代码执行漏洞:攻击者可以在反序列化期间执行恶意代码,向远程攻击公开应用程序。
- 拒绝服务攻击:恶意剪贴板数据可能会消耗过多的内存或 CPU 资源,从而导致崩溃或不稳定。
- 信息泄露风险:攻击者可能会从内存中提取敏感数据。
- 无安全边界:格式从根本上不安全,配置设置无法保护它。
仅当更新应用程序以使用新类型安全 API 时,才启用此支持作为临时网桥。
安装兼容性包
将不受支持的 BinaryFormatter 兼容性包添加到项目。 此包为BinaryFormatter操作提供必要的运行时支持。
<ItemGroup>
<PackageReference Include="System.Runtime.Serialization.Formatters" Version="10.0.0*-*"/>
</ItemGroup>
注释
此包标记为不受支持且已弃用。 它仅用于迁移期间的临时兼容性。
在项目中启用不安全的序列化
在项目文件中将 EnableUnsafeBinaryFormatterSerialization 属性设置为 true。 此属性告知编译器允许 BinaryFormatter 使用:
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
</PropertyGroup>
如果没有此设置,应用程序在尝试使用 BinaryFormatter API 时会生成编译错误。
配置 Windows 窗体运行时开关
创建或更新应用程序 runtimeconfig.json 的文件以启用特定于 Windows 窗体的剪贴板开关。 此配置允许剪贴板操作在必要时回退到 BinaryFormatter。
{
"runtimeOptions": {
"configProperties": {
"Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization": true
}
}
}
重要
如果没有此特定的运行时开关,即使启用了常规序列化支持,剪贴板操作也不会回退到BinaryFormatter。 此开关专门用于 Windows 窗体和 WPF 剪贴板功能。
实现以安全为中心的类型解析程序
即使启用了BinaryFormatter,也要实现类型解析程序以限制反序列化为显式批准的类型。 类型解析程序提供针对恶意有效负载攻击的唯一防御。
创建安全类型解析程序
// Create a security-focused type resolver
private static Type SecureTypeResolver(TypeName typeName)
{
// Explicit allow-list of permitted types—add only what you need
var allowedTypes = new Dictionary<string, Type>
{
["MyApp.Person"] = typeof(Person),
["MyApp.AppSettings"] = typeof(AppSettings),
["System.String"] = typeof(string),
["System.Int32"] = typeof(int),
// Add only the specific types your application requires
};
// Only allow explicitly listed types - exact string match required
if (allowedTypes.TryGetValue(typeName.FullName, out Type allowedType))
{
return allowedType;
}
// Reject any type not in the allow-list with clear error message
throw new InvalidOperationException(
$"Type '{typeName.FullName}' is not permitted for clipboard deserialization");
}
' Create a security-focused type resolver
Private Shared Function SecureTypeResolver(typeName As TypeName) As Type
' Explicit allow-list of permitted types—add only what you need
Dim allowedTypes As New Dictionary(Of String, Type) From {
{"MyApp.Person", GetType(Person)},
{"MyApp.AppSettings", GetType(AppSettings)},
{"System.String", GetType(String)},
{"System.Int32", GetType(Integer)}
} ' Add only the specific types your application requires
' Only allow explicitly listed types - exact string match required
Dim allowedType As Type = Nothing
If allowedTypes.TryGetValue(typeName.FullName, allowedType) Then
Return allowedType
End If
' Reject any type not in the allow-list with clear error message
Throw New InvalidOperationException(
$"Type '{typeName.FullName}' is not permitted for clipboard deserialization")
End Function
将类型解析程序与剪贴板作配合使用
// Use the resolver with clipboard operations
private static Type SecureTypeResolver(TypeName typeName)
{
// Implementation from SecureTypeResolver example
// ... (allow-list implementation here)
throw new InvalidOperationException($"Type '{typeName.FullName}' is not permitted");
}
public static void UseSecureTypeResolver()
{
// Retrieve legacy data using the secure type resolver
if (Clipboard.TryGetData("LegacyData", SecureTypeResolver, out MyCustomType data))
{
ProcessLegacyData(data);
}
else
{
Console.WriteLine("No compatible data found on clipboard");
}
}
' Use the resolver with clipboard operations
Private Shared Function SecureTypeResolver(typeName As TypeName) As Type
' Implementation from SecureTypeResolver example
' ... (allow-list implementation here)
Throw New InvalidOperationException($"Type '{typeName.FullName}' is not permitted")
End Function
Public Shared Sub UseSecureTypeResolver()
' Retrieve legacy data using the secure type resolver
Dim data As MyCustomType = Nothing
If Clipboard.TryGetData("LegacyData", AddressOf SecureTypeResolver, data) Then
ProcessLegacyData(data)
Else
Console.WriteLine("No compatible data found on clipboard")
End If
End Sub
类型解析程序的安全准则
实现类型解析程序时,请遵循以下基本安全准则:
使用显式允许列表
- 默认拒绝:仅允许显式列出的类型。
- 无通配符:避免模式匹配或基于命名空间的权限。
- 精确匹配:需要类型名称的确切字符串匹配。
验证所有输入
- 类型名称验证:确保类型名称与预期格式匹配。
- 程序集限制:将类型限制为已知受信任的程序集。
- 版本检查:考虑特定于版本的类型限制。
安全地处理未知类型
- 抛出异常:始终对未经授权的类型抛出异常。
- 日志尝试:考虑记录未经授权的访问尝试。
- 清除错误消息:提供调试的特定拒绝原因。
定期维护
- 定期审核:查看和更新允许的类型列表。
- 删除未使用的类型:消除不再需要的类型的权限。
- 文档决策:保持关于每种类型为何被允许的清晰文档。
测试配置
配置 BinaryFormatter 支持后,测试应用程序以确保其正常工作:
- 验证剪贴板操作:测试使用自定义类型进行数据的存储和检索。
- 测试类型解析程序:确认未经授权的类型已正确拒绝。
- 监视安全性:监视任何意外的类型解析请求。
- 性能测试:确保类型解析程序不会显著影响性能。
public static void TestBinaryFormatterConfiguration()
{
// Test data to verify configuration
var testPerson = new Person { Name = "Test User", Age = 30 };
try
{
// Test storing data (this should work with proper configuration)
Clipboard.SetData("TestPerson", testPerson);
Console.WriteLine("Successfully stored test data on clipboard");
// Test retrieving with type resolver
if (Clipboard.TryGetData("TestPerson", SecureTypeResolver, out Person retrievedPerson))
{
Console.WriteLine($"Successfully retrieved: {retrievedPerson.Name}, Age: {retrievedPerson.Age}");
}
else
{
Console.WriteLine("Failed to retrieve test data");
}
// Test that unauthorized types are rejected
try
{
Clipboard.TryGetData("TestPerson", UnauthorizedTypeResolver, out Person _);
Console.WriteLine("ERROR: Unauthorized type was not rejected!");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"SUCCESS: Unauthorized type properly rejected - {ex.Message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Configuration test failed: {ex.Message}");
}
}
private static Type SecureTypeResolver(TypeName typeName)
{
var allowedTypes = new Dictionary<string, Type>
{
["ClipboardExamples.Person"] = typeof(Person),
};
if (allowedTypes.TryGetValue(typeName.FullName, out Type allowedType))
{
return allowedType;
}
throw new InvalidOperationException($"Type '{typeName.FullName}' is not permitted");
}
private static Type UnauthorizedTypeResolver(TypeName typeName)
{
// Intentionally restrictive resolver to test rejection
throw new InvalidOperationException($"No types are permitted by this test resolver");
}
Public Shared Sub TestBinaryFormatterConfiguration()
' Test data to verify configuration
Dim testPerson As New Person With {.Name = "Test User", .Age = 30}
Try
' Test storing data (this should work with proper configuration)
Clipboard.SetData("TestPerson", testPerson)
Console.WriteLine("Successfully stored test data on clipboard")
' Test retrieving with type resolver
Dim retrievedPerson As Person = Nothing
If Clipboard.TryGetData("TestPerson", AddressOf SecureTypeResolver, retrievedPerson) Then
Console.WriteLine($"Successfully retrieved: {retrievedPerson.Name}, Age: {retrievedPerson.Age}")
Else
Console.WriteLine("Failed to retrieve test data")
End If
' Test that unauthorized types are rejected
Try
Dim testResult As Person = Nothing
Clipboard.TryGetData("TestPerson", AddressOf UnauthorizedTypeResolver, testResult)
Console.WriteLine("ERROR: Unauthorized type was not rejected!")
Catch ex As InvalidOperationException
Console.WriteLine($"SUCCESS: Unauthorized type properly rejected - {ex.Message}")
End Try
Catch ex As Exception
Console.WriteLine($"Configuration test failed: {ex.Message}")
End Try
End Sub
Private Shared Function SecureTypeResolver(typeName As TypeName) As Type
Dim allowedTypes As New Dictionary(Of String, Type) From {
{"ClipboardExamples.Person", GetType(Person)}
}
Dim allowedType As Type = Nothing
If allowedTypes.TryGetValue(typeName.FullName, allowedType) Then
Return allowedType
End If
Throw New InvalidOperationException($"Type '{typeName.FullName}' is not permitted")
End Function
Private Shared Function UnauthorizedTypeResolver(typeName As TypeName) As Type
' Intentionally restrictive resolver to test rejection
Throw New InvalidOperationException($"No types are permitted by this test resolver")
End Function
规划迁移策略
虽然 BinaryFormatter 支持提供临时性兼容性,但应制定计划以迁移到新的类型安全 API:
- 确定使用情况:使用自定义类型记录所有剪贴板操作。
- 确定迁移优先级:首先关注最具安全敏感性的操作。
- 以增量方式更新:一次更新一个操作以降低风险。
- 全面测试:确保新实现提供等效功能。
- 删除 BinaryFormatter:完成迁移后禁用支持。
清理资源
迁移到新的类型安全剪贴板 API 后,请删除 BinaryFormatter 配置以提高安全性:
- 删除
System.Runtime.Serialization.Formatters包引用。 -
EnableUnsafeBinaryFormatterSerialization从项目文件中删除该属性。 - 从
runtimeconfig.json中删除Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization设置。 - 删除不再需要的类型解析程序实现。