启用 BinaryFormatter 剪贴板支持(不建议)

注意

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 支持后,测试应用程序以确保其正常工作:

  1. 验证剪贴板操作:测试使用自定义类型进行数据的存储和检索。
  2. 测试类型解析程序:确认未经授权的类型已正确拒绝。
  3. 监视安全性:监视任何意外的类型解析请求。
  4. 性能测试:确保类型解析程序不会显著影响性能。
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:

  1. 确定使用情况:使用自定义类型记录所有剪贴板操作。
  2. 确定迁移优先级:首先关注最具安全敏感性的操作。
  3. 以增量方式更新:一次更新一个操作以降低风险。
  4. 全面测试:确保新实现提供等效功能。
  5. 删除 BinaryFormatter:完成迁移后禁用支持。

清理资源

迁移到新的类型安全剪贴板 API 后,请删除 BinaryFormatter 配置以提高安全性:

  1. 删除System.Runtime.Serialization.Formatters包引用。
  2. EnableUnsafeBinaryFormatterSerialization从项目文件中删除该属性。
  3. runtimeconfig.json中删除Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization设置。
  4. 删除不再需要的类型解析程序实现。