代码访问安全性和 ADO.NET

.NET Framework 提供基于角色的安全性和代码访问安全性 (CAS),这两种安全性都可以通过公共语言运行库 (CLR) 提供的公共基础结构实现。 对于非托管代码,大多数应用程序都可以使用用户或主体权限执行。 因此,当拥有提升权限的用户运行恶意软件或包含错误的软件时,计算机系统可能会受到损坏并危及私有数据。

相对而言,.NET Framework 中执行的托管代码包括单独应用于代码的代码访问安全性。 是否允许运行代码取决于代码的来源或代码标识的其他方面,而不仅仅是主体标识。 这样可以减小滥用托管代码的可能性。

注意

代码访问安全性 (CAS) 已在所有版本的 .NET Framework 和 .NET 中弃用。 如果使用与 CAS 相关的 API,最新版本的 .NET 不会遵循 CAS 注释,并会生成错误。 开发人员应寻求用于完成安全任务的替代方法。

代码访问权限

在执行代码时,代码会提供通过 CLR 安全系统计算的证据。 通常,此证据由代码的来源(包括 URL、站点和区域)以及确保程序集标识的数字签名组成。

CLR 仅允许代码执行代码具有执行权限的那些操作。 代码可以请求权限,而这些请求需要基于管理员设置的安全策略。

注意

在 CLR 中指定的代码不能为自身授予权限。 例如,代码可以请求并获得比安全策略允许的权限少的权限,但决不会获得比安全策略允许的权限多的权限。 在授予权限时,应该从无权限开始,然后为要执行的特定任务添加最少的权限。 一开始就使用所有权限,然后拒绝各个权限会导致应用程序不安全,应用程序可能会授予不必要的权限,从而使应用程序无意中包含安全漏洞。 有关详细信息,请参阅配置安全策略安全策略管理

代码访问权限有三种类型:

  • Code access permissionsCodeAccessPermission 类派生。 需要具有权限才能访问受保护的资源(如文件和环境变量)和执行受保护的操作(如访问托管代码)。
  • Identity permissions表示标识程序集的特征。 对程序集授予权限需要基于证据,而证据可以包括如数字签名或代码来源等项。 标识权限也从 CodeAccessPermission 基类派生。
  • Role-based security permissions基于主体是否具有指定标识或是否是指定角色的成员。 PrincipalPermission 类允许对活动主体进行声明性和强制性权限检查。

为了确定代码是否获得了访问某一资源或执行某一操作的授权,运行库的安全系统将遍历调用堆栈,将每个调用方已获得的权限与要求的权限进行比较。 如果调用堆栈中的任何调用方没有要求的权限,则会引发 SecurityException 并拒绝访问。

请求权限

请求权限的目的是通知运行库您的应用程序要求哪些权限才能运行,并确保应用程序只接收到实际需要的权限。 例如,如果您的应用程序需要将数据写入本地磁盘,则需要 FileIOPermission。 如果尚未授予该权限,则在应用程序尝试写入磁盘时将失败。 不过,如果应用程序请求 FileIOPermission 并且尚未授予该权限,则应用程序一开始即会生成异常,因此将不会加载。

在应用程序只需从磁盘读取数据的情况下,您可以请求永远不为应用程序授予任何写入权限。 在出现 Bug 或受到恶意攻击时,你的代码将不会损坏它所操作的数据。 有关详细信息,请参阅请求权限

基于角色的安全性和 CAS

同时实现基于角色的安全性和代码访问安全性 (CAS) 可增强应用程序的整体安全性。 基于角色的安全性可以基于 Windows 帐户或自定义标识,使有关安全主体的信息可用于当前线程。 此外,通常还要求应用程序基于用户提供的凭据提供对数据或资源的访问。 通常情况下,这种应用程序会检查用户的角色,并根据这些角色提供对资源的访问。

基于角色的安全性使组件能够在运行时标识当前用户及其关联的角色。 然后使用 CAS 策略映射此信息以确定运行时授予的一组权限。 对于指定的应用程序域,主机可以更改基于角色的默认安全策略并设置表示用户的默认安全主体和与该用户关联的角色。

CLR 使用权限来实现其用于对托管代码实施限制的机制。 基于角色的安全权限提供一种机制,用于发现用户(或代表该用户的代理)是否具有特定标识或者是否是指定角色的成员。 有关详细信息,请参阅安全权限

根据要生成的应用程序类型,还应考虑在数据库中实现基于角色的权限。 有关 SQL Server 中基于角色的安全性的详细信息,请参阅 SQL Server 安全性

程序集

程序集构成 .NET Framework 应用程序部署、版本控制、重复使用、激活范围和安全权限的基本单元。 程序集提供类型和资源的集合,二者结合在一起构成功能的逻辑单元。 对于 CLR,类型不存在于程序集的上下文之外。 有关创建和部署程序集的详细信息,请参阅使用程序集编程

具有强名称的程序集

强名称(或数字签名)由程序集的标识组成,该标识包括程序集的简单文本名称、版本号和区域性信息(如果提供)、公钥和数字签名。 数字签名使用相应私钥从程序集文件生成。 程序集文件包含程序集清单,该清单包含组成程序集的所有文件的名称和哈希。

强命名程序集可为应用程序或组件提供唯一的标识,其他软件可以使用该标识显式引用应用程序或组件。 强命名可以保护程序集,防止包含恶意代码的程序集冒充。 强命名还可以保证组件的不同版本之间的版本一致性。 对于将要部署到全局程序集缓存 (GAC) 的程序集,必须进行强命名。 有关详细信息,请参阅创建和使用具有强名称的程序集

ADO.NET 2.0 中的部分信任

在 ADO.NET 2.0 中,适用于 SQL Server 的 .NET Framework 数据提供程序、适用于 OLE DB 的 .NET Framework 数据提供程序、适用于 ODBC 的 .NET Framework 数据提供程序和适用于 Oracle 的 .NET Framework 数据提供程序现在全部可以在部分信任的环境中运行。 在以前版本的 .NET Framework 中,非完全信任的应用程序仅支持 System.Data.SqlClient

使用 SQL Server 提供程序的部分信任应用程序必须至少具有执行和 SqlClientPermission 权限。

部分信任的权限属性

对于部分信任的方案,可以使用 SqlClientPermissionAttribute 成员进一步限制 SQL Server .NET Framework 数据提供程序可以使用的功能。

下表列出了可用的 SqlClientPermissionAttribute 属性及其说明:

权限属性 说明
Action 获取或设置安全性操作。 从 SecurityAttribute 继承。
AllowBlankPassword 启用或禁用连接字符串中空白密码。 有效值为 true(启用空白密码的使用)和 false(禁用空白密码的使用)。 从 DBDataPermissionAttribute 继承。
ConnectionString 标识允许的连接字符串。 可标识多个连接字符串。 注意:请勿在连接字符串中包含用户 ID 或密码。 此版本中,不能使用 .NET Framework 配置工具更改连接字符串限制。

DBDataPermissionAttribute 继承。
KeyRestrictions 标识允许或不允许的连接字符串参数。 连接字符串参数以 <参数名称>= 格式进行标识。 可指定多个参数,并用分号 (;) 进行分隔。 注意:如果未指定 KeyRestrictions,但将 KeyRestrictionBehavior 属性设置为 AllowOnlyPreventUsage,则不允许使用其他连接字符串参数。 从 DBDataPermissionAttribute 继承。
KeyRestrictionBehavior 将连接字符串参数标识为唯一允许的附加参数 (AllowOnly),或标识不允许的附加参数 (PreventUsage)。 AllowOnly 是默认值。 从 DBDataPermissionAttribute 继承。
TypeID 在派生类中实现此属性时获取唯一标识符。 从 Attribute 继承。
Unrestricted 表明是否声明对该资源的无限制权限。 从 SecurityAttribute 继承。

ConnectionString 语法

下面的示例演示如何使用配置文件的 connectionStrings 元素仅允许使用特定的连接字符串。 有关从配置文件中存储和检索连接字符串的更多信息,请参阅连接字符串

<connectionStrings>
  <add name="DatabaseConnection"
    connectionString="Data Source=(local);Initial
    Catalog=Northwind;Integrated Security=true;" />
</connectionStrings>

重要

Microsoft 建议使用最安全的可用身份验证流。 如果要连接到 Azure SQL,建议使用 Azure 资源的托管标识这种身份验证方法。

KeyRestrictions 语法

以下示例启用相同的连接字符串,启用了 EncryptPacket Size 连接字符串选项,但是限制任何其他连接字符串选项的使用。

<connectionStrings>
  <add name="DatabaseConnection"
    connectionString="Data Source=(local);Initial
    Catalog=Northwind;Integrated Security=true;"
    KeyRestrictions="Encrypt=;Packet Size=;"
    KeyRestrictionBehavior="AllowOnly" />
</connectionStrings>

重要

Microsoft 建议使用最安全的可用身份验证流。 如果要连接到 Azure SQL,建议使用 Azure 资源的托管标识这种身份验证方法。

包含 PreventUsage 的 KeyRestrictionBehavior 的语法

下面的示例启用相同的连接字符串,并允许使用 User IdPasswordPersist Security Info 以外的所有其他连接参数。

<connectionStrings>
  <add name="DatabaseConnection"
    connectionString="Data Source=(local);Initial
    Catalog=Northwind;Integrated Security=true;"
    KeyRestrictions="User Id=;Password=;Persist Security Info=;"
    KeyRestrictionBehavior="PreventUsage" />
</connectionStrings>

重要

Microsoft 建议使用最安全的可用身份验证流。 如果要连接到 Azure SQL,建议使用 Azure 资源的托管标识这种身份验证方法。

包含 AllowOnly 的 KeyRestrictionBehavior 的语法

以下示例启用两个连接字符串,其中还包含 Initial CatalogConnection TimeoutEncryptPacket Size 参数。 而所有其他的连接字符串参数都被限制。

<connectionStrings>
  <add name="DatabaseConnection"
    connectionString="Data Source=(local);Initial
    Catalog=Northwind;Integrated Security=true;"
    KeyRestrictions="Initial Catalog;Connection Timeout=;
       Encrypt=;Packet Size=;"
    KeyRestrictionBehavior="AllowOnly" />

  <add name="DatabaseConnection2"
    connectionString="Data Source=SqlServer2;Initial
    Catalog=Northwind2;Integrated Security=true;"
    KeyRestrictions="Initial Catalog;Connection Timeout=;
       Encrypt=;Packet Size=;"
    KeyRestrictionBehavior="AllowOnly" />
</connectionStrings>

重要

Microsoft 建议使用最安全的可用身份验证流。 如果要连接到 Azure SQL,建议使用 Azure 资源的托管标识这种身份验证方法。

启用具有自定义权限集的部分信任

要对特定区域启用 System.Data.SqlClient 权限,系统管理员必须创建自定义的权限集,并将其设置为特定区域的权限集。 不能修改默认权限集(如 LocalIntranet)。 例如,要对具有 LocalIntranetZone 的代码添加 System.Data.SqlClient 权限,则系统管理员可复制 LocalIntranet 的权限集,将其重命名为“CustomLocalIntranet”,添加 System.Data.SqlClient 权限,再使用 Caspol.exe(代码访问安全性策略工具)导入 CustomLocalIntranet 权限集,然后将 LocalIntranet_Zone 权限集设置为 CustomLocalIntranet。

示例权限集

下面是在部分受信任方案中,SQL Server .NET Framework 数据提供程序的示例权限集。 有关创建自定义权限集的信息,请参阅使用 Caspol.exe 配置权限集

<PermissionSet class="System.Security.NamedPermissionSet"
  version="1"
  Name="CustomLocalIntranet"
  Description="Custom permission set given to applications on
    the local intranet">

<IPermission class="System.Data.SqlClient.SqlClientPermission, System.Data, Version=2.0.0000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
version="1"
AllowBlankPassword="False">
<add ConnectionString="Data Source=(local);Integrated Security=true;"
 KeyRestrictions="Initial Catalog=;Connection Timeout=;
   Encrypt=;Packet Size=;"
 KeyRestrictionBehavior="AllowOnly" />
 </IPermission>
</PermissionSet>

重要

Microsoft 建议使用最安全的可用身份验证流。 如果要连接到 Azure SQL,建议使用 Azure 资源的托管标识这种身份验证方法。

使用安全权限验证 ADO.NET 代码访问

对于部分信任方案,可以通过指定 SqlClientPermissionAttribute 来要求代码中的特定方法具有 CAS 特权。 如果当前受限制的安全策略不允许该权限,在运行代码之前将引发异常。 有关安全策略的详细信息,请参阅安全策略管理安全策略最佳做法

示例

以下示例演示如何编写要求特定连接字符串的代码。 该示例模拟拒绝为 System.Data.SqlClient 授予无限制权限的过程,系统管理员在实际工作中将会使用 CAS 策略实现该过程。

重要

在设计 ADO.NET 的 CAS 权限时,正确的模式是以限制性最强的情况开始(无任何权限),然后添加代码执行特定任务所需的特定权限。 相反的模式是一开始就授予所有权限,然后拒绝特定权限,这样做是不安全的,因为表达同一连接字符串可以有许多方式。 例如,如果一开始就授予所有权限,然后尝试拒绝使用连接字符串“server=someserver”,则仍将允许使用“server=someserver.mycompany.com”。 通过在开始时始终不授予任何权限,可以降低权限集中存在漏洞的几率。

下面的代码演示 SqlClient 如何执行安全请求,如果没有相应的 CAS 权限,将引发 SecurityException。 控制台窗口中显示 SecurityException 输出。

using System;
using System.Data;
using System.Data.SqlClient;
using System.Security;
using System.Security.Permissions;

namespace PartialTrustTopic
{
   public class PartialTrustHelper : MarshalByRefObject
   {
      public void TestConnectionOpen(string connectionString)
      {
         // Try to open a connection.
         using (SqlConnection connection = new SqlConnection(connectionString))
         {
            connection.Open();
         }
      }
   }

   class Program
   {
      static void Main(string[] args)
      {
         TestCAS("secure-connnection-string1", "secure-connnection-string2");
      }

      static void TestCAS(string connectString1, string connectString2)
      {
         // Create permission set for sandbox AppDomain.
         // This example only allows execution.
         PermissionSet permissions = new PermissionSet(PermissionState.None);
         permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));

         // Create sandbox AppDomain with permission set that only allows execution,
         // and has no SqlClientPermissions.
         AppDomainSetup appDomainSetup = new AppDomainSetup();
         appDomainSetup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
         AppDomain firstDomain = AppDomain.CreateDomain("NoSqlPermissions", null, appDomainSetup, permissions);

         // Create helper object in sandbox AppDomain so that code can be executed in that AppDomain.
         Type helperType = typeof(PartialTrustHelper);
         PartialTrustHelper firstHelper = (PartialTrustHelper)firstDomain.CreateInstanceAndUnwrap(helperType.Assembly.FullName, helperType.FullName);

         try
         {
            // Attempt to open a connection in the sandbox AppDomain.
            // This is expected to fail.
            firstHelper.TestConnectionOpen(connectString1);
            Console.WriteLine("Connection opened, unexpected.");
         }
         catch (System.Security.SecurityException ex)
         {
            Console.WriteLine("Failed, as expected: {0}",
                ex.FirstPermissionThatFailed);

            // Uncomment the following line to see Exception details.
            // Console.WriteLine("BaseException: " + ex.GetBaseException());
         }

         // Add permission for a specific connection string.
         SqlClientPermission sqlPermission = new SqlClientPermission(PermissionState.None);
         sqlPermission.Add(connectString1, "", KeyRestrictionBehavior.AllowOnly);

         permissions.AddPermission(sqlPermission);

         AppDomain secondDomain = AppDomain.CreateDomain("OneSqlPermission", null, appDomainSetup, permissions);
         PartialTrustHelper secondHelper = (PartialTrustHelper)secondDomain.CreateInstanceAndUnwrap(helperType.Assembly.FullName, helperType.FullName);

         // Try connection open again, it should succeed now.
         try
         {
            secondHelper.TestConnectionOpen(connectString1);
            Console.WriteLine("Connection opened, as expected.");
         }
         catch (System.Security.SecurityException ex)
         {
            Console.WriteLine("Unexpected failure: {0}", ex.Message);
         }

         // Try a different connection string. This should fail.
         try
         {
            secondHelper.TestConnectionOpen(connectString2);
            Console.WriteLine("Connection opened, unexpected.");
         }
         catch (System.Security.SecurityException ex)
         {
            Console.WriteLine("Failed, as expected: {0}", ex.Message);
         }
      }
   }
}
Imports System.Data.SqlClient
Imports System.Security
Imports System.Security.Permissions

Namespace PartialTrustTopic
    Public Class PartialTrustHelper
        Inherits MarshalByRefObject
        Public Sub TestConnectionOpen(ByVal connectionString As String)
            ' Try to open a connection.
            Using connection As New SqlConnection(connectionString)
                connection.Open()
            End Using
        End Sub
    End Class

    Class Program
        Public Shared Sub Main(ByVal args As String())
            TestCAS("Data Source=(local);Integrated Security=true", "Data Source=(local);Integrated Security=true;Initial Catalog=Test")
        End Sub

        Public Shared Sub TestCAS(ByVal connectString1 As String, ByVal connectString2 As String)
            ' Create permission set for sandbox AppDomain.
            ' This example only allows execution.
            Dim permissions As New PermissionSet(PermissionState.None)
            permissions.AddPermission(New SecurityPermission(SecurityPermissionFlag.Execution))

            ' Create sandbox AppDomain with permission set that only allows execution,
            ' and has no SqlClientPermissions.
            Dim appDomainSetup As New AppDomainSetup()
            appDomainSetup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase
            Dim firstDomain As AppDomain = AppDomain.CreateDomain("NoSqlPermissions", Nothing, appDomainSetup, permissions)

            ' Create helper object in sandbox AppDomain so that code can be executed in that AppDomain.
            Dim helperType As Type = GetType(PartialTrustHelper)
            Dim firstHelper As PartialTrustHelper = DirectCast(firstDomain.CreateInstanceAndUnwrap(helperType.Assembly.FullName, helperType.FullName), PartialTrustHelper)

            Try
                ' Attempt to open a connection in the sandbox AppDomain.
                ' This is expected to fail.
                firstHelper.TestConnectionOpen(connectString1)
                Console.WriteLine("Connection opened, unexpected.")
            Catch ex As System.Security.SecurityException

                ' Uncomment the following line to see Exception details.
                ' Console.WriteLine("BaseException: " + ex.GetBaseException());
                Console.WriteLine("Failed, as expected: {0}", ex.FirstPermissionThatFailed)
            End Try

            ' Add permission for a specific connection string.
            Dim sqlPermission As New SqlClientPermission(PermissionState.None)
            sqlPermission.Add(connectString1, "", KeyRestrictionBehavior.AllowOnly)

            permissions.AddPermission(sqlPermission)

            Dim secondDomain As AppDomain = AppDomain.CreateDomain("OneSqlPermission", Nothing, appDomainSetup, permissions)
            Dim secondHelper As PartialTrustHelper = DirectCast(secondDomain.CreateInstanceAndUnwrap(helperType.Assembly.FullName, helperType.FullName), PartialTrustHelper)

            ' Try connection open again, it should succeed now.
            Try
                secondHelper.TestConnectionOpen(connectString1)
                Console.WriteLine("Connection opened, as expected.")
            Catch ex As System.Security.SecurityException
                Console.WriteLine("Unexpected failure: {0}", ex.Message)
            End Try

            ' Try a different connection string. This should fail.
            Try
                secondHelper.TestConnectionOpen(connectString2)
                Console.WriteLine("Connection opened, unexpected.")
            Catch ex As System.Security.SecurityException
                Console.WriteLine("Failed, as expected: {0}", ex.Message)
            End Try
        End Sub
    End Class
End Namespace

在控制台窗口中应看到以下输出:

Failed, as expected: <IPermission class="System.Data.SqlClient.
SqlClientPermission, System.Data, Version=2.0.0.0,
  Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1"
  AllowBlankPassword="False">
<add ConnectionString="Data Source=(local);Initial Catalog=
  Northwind;Integrated Security=SSPI" KeyRestrictions=""
KeyRestrictionBehavior="AllowOnly"/>
</IPermission>

Connection opened, as expected.
Failed, as expected: Request failed.

重要

Microsoft 建议使用最安全的可用身份验证流。 如果要连接到 Azure SQL,建议使用 Azure 资源的托管标识这种身份验证方法。

与非托管代码的互操作性

在 CLR 外部运行的代码称为非托管代码。 因此,安全机制(如 CAS)不能应用于非托管代码。 COM 组件、ActiveX 接口和 Windows API 函数都是非托管代码的示例。 在执行非托管代码时应考虑特殊安全注意事项,以便不会危害应用程序的整体安全性。 有关详细信息,请参阅与非托管代码交互操作

.NET Framework 可以通过 COM 互操作提供访问,因此还支持与现有 COM 组件的向后兼容。 通过使用 COM 互操作工具导入相关的 COM 类型,可以将 COM 组件合并到 .NET Framework 应用程序中。 一旦导入后,就可以使用 COM 类型了。 通过将程序集元数据导出到类型库并将托管组件注册为 COM 组件,COM 互操作还可以使 COM 客户端访问托管代码。 有关详细信息,请参阅高级 COM 互操作性

请参阅