构建安全的程序集

来源:互联网 发布:网络防火墙的作用 编辑:程序博客网 时间:2024/05/16 01:25
本页内容
本模块内容本模块内容
目标目标
适用范围适用范围
如何使用本模块如何使用本模块
威胁与对策威胁与对策
特权代码特权代码
程序集设计注意事项程序集设计注意事项
类设计注意事项类设计注意事项
强名称强名称
授权授权
异常管理异常管理
文件 I/O文件 I/O
事件日志事件日志
注册表注册表
数据访问数据访问
非托管代码非托管代码
委派委派
序列化序列化
线程线程
反射反射
混淆混淆
加密加密
小结小结
其他资源其他资源

本模块内容

程序集是 .NET Framework 应用程序的构建块,是部署、版本控制和重用的单元。它们还是代码访问安全的信任单元(程序集中的所有代码都受到同等程度的信任)。

本模块开始列出并解释了常见程序集威胁及与其关联的对策。然后是您必需解决的安全区域的综合列表,以便改进程序集的安全设计与实现。这包括评估部署注意事项、遵循面向对象编程的可靠经验、使代码具有防篡改性、确保不向呼叫方泄露内部系统级信息,以及限制可以调用代码的人员。

返回页首返回页首

目标

使用本模块可以实现:

通过简单的、经证实的编码技术提高程序集的安全性。

通过良好设计的接口和面向对象编程的可靠技术来缩小攻击面。

使用强名称使程序集具有防篡改性。

减少与调用非托管代码相关的风险。

编写安全的资源访问代码,包括文件 I/O、注册表、事件日志、数据库和网络访问代码。

了解解决常见程序集威胁需要应用的对策,包括特权提升、代码注入、信息泄露和篡改。

返回页首返回页首

适用范围

本模块适用于下列产品和技术:

Microsoft Windows Server 2000 和 2003

Microsoft .NET Framework 1.1 和 ASP.NET 1.1

返回页首返回页首

如何使用本模块

为了充分理解本模块内容,请:

将本模块与模块 8 代码访问安全的实践一起使用。模块 8 显示了如何使用代码访问安全功能以进一步提高程序集的安全性。

使用相应的检查表。要获得两个模块的最佳做法和建议的汇总检查表,请参阅本指南“检查表”部分中的检查表:托管代码的安全检查。

您还应注意下列问题,它们与
.NET Framework 版本 1.0 和 1.1 相关:

由于要求完全信任调用方的核心 .NET 程序集的数量,很难创建部分信任的环境。

不能为 ASP.NET 网页使用强名称。

向全局程序集缓存发布程序集是对代码应用 Sandbox 技术的唯一解决方案。?

要在程序集中使用强名称,并将它们发布到全局程序集缓存中,您必须具有充分的特权,以便在 Web 服务器上执行 Sn.exe 和 Gacutil.exe 之类的命令。

ASP.NET 主机不加载验证码证据,这说明您不能用它为 ASP.NET Web 应用程序建立安全策略。

返回页首返回页首

威胁与对策

了解威胁和常见攻击类型有助于确定适当的对策,并使您可以构建更安全更可靠的程序集。主要威胁包括:

未授权访问和/或特权提升

代码注入

信息泄露

篡改

图 7.1 说明了这些比较严重的威胁。

 程序集级威胁

图 7.1
程序集级威胁

未授权访问和/或特权提升

未授权访问的风险会导致特权提升,未授权用户或代码可以调用您的程序集,执行特权操作并访问受限制的资源。

漏洞

可以导致未授权访问和特权提升的漏洞包括:

基于角色的授权较弱或缺少

内部类型和类型成员被意外泄露

不安全地使用了代码访问安全断言和链接请求

允许派生任何代码的未密封和未受限制的基类

攻击

常见攻击包括:

引诱性攻击,恶意代码通过受信任的中间程序集访问您的程序集,从而绕过授权机制

恶意代码直接调用不属于程序集公共 API 的类、从而绕过访问控制的攻击

对策

可以用来阻止未授权访问和特权提升的对策包括:

使用基于角色的授权为所有公共类和类成员提供访问控制。

限制类型和成员可见性,以限制可以公共访问的代码。

对特权代码应用 Sandbox 技术,并确保准许调用代码请求相应的权限。

密封非基类或使用代码访问安全限制继承。

代码注入

通过代码注入,攻击者可以利用程序集的进程级安全上下文执行任意代码。如果程序集调用了非托管代码并且在特权帐户下运行,则会增大风险。

漏洞

可以导致代码注入的漏洞包括:

输入验证较弱,尤其是在程序集调入非托管代码的位置。

接受部分信任代码的委派

过分特权进程帐户

攻击

常见代码注入攻击包括:

缓冲区溢出

调用不受信任的源的委派

对策

可以用来阻止代码注入的对策包括:

验证输入参数。

验证传递给非托管 API 的数据。

不要接受来自不受信任的源的委派。

使用强类型委派,并在调用委派之前拒绝权限。

要进一步减小风险,请使用具有最少特权的帐户运行程序集。

信息泄露

如果程序集向合法或恶意用户等泄漏了敏感数据,如异常详细信息和明文机密,该程序集可能会造成信息泄露。此外,将程序集的 Microsoft Intermediate Language (MSIL) 反向工程为源代码比使用二进制机器代码进行转换容易。 这就对知识产权造成了威胁。

漏洞

可以导致信息泄露的漏洞包括:

异常处理机制较弱或不正式

代码中存在硬编码机密

攻击

常见攻击包括:

尝试通过向程序集发送格式不正确的输入而导致错误

在程序集上使用 ILDASM 以窃取机密

对策

可以用来阻止信息泄露的对策包括:

可靠的输入验证

使用结构化异常处理机制,并向客户端返回一般性错误

不要在代码中存储机密

使用混淆工具,以防止反编译并保护知识产权

篡改

篡改的风险在于,通过改变二进制 DLL 或 EXE 程序集文件中的 MSIL 指令来修改程序集。

漏洞

使程序集易受篡改攻击的主要漏洞是缺乏强名称签名。

攻击

常见攻击包括:

直接操作 MSIL 指令

反向工程 MSIL 指令

对策

要应对篡改威胁,可使用强名称为具有私钥的程序集签名。加载具有签名的程序集时,公共语言运行库将检测是否以任何方式对该程序集进行了修改,如果发现修改,则不加载该程序集。

返回页首返回页首

特权代码

设计和生成安全程序集时,可以标识特权代码。这对于代码访问安全具有重要的意义。特权代码是托管代码,它访问受保护的资源,或执行其他安全敏感操作,如调用非托管代码、使用序列化或使用反射。它之所以被称作特权代码,是因为它必须由代码访问安全策略授予权限之后才能运行。非特权代码只需要执行权限。

特权资源

代码需要代码访问安全权限的资源类型包括文件系统、数据库、注册表、事件日志、Web Service、套接字、DNS 数据库、目录服务和环境变量。

特权操作

代码需要代码访问安全权限的其他特权操作包括调用非托管代码、使用序列化、使用反射、创建和控制应用程序域、创建 Principal 对象和操作安全策略。

有关访问资源或执行特权操作所需的特定类型的代码访问安全权限的详细信息,请参阅模块 8 代码访问安全的实践中的“特权代码”。

返回页首返回页首

程序集设计注意事项

设计时需要考虑的一个最重要的问题是程序集目标环境的信任级别,它会影响授予您的代码以及调用您代码的代码的代码访问安全权限。这种信任级别是由管理员定义的代码访问安全策略确定的,它将影响您的代码可以访问的资源类型,以及它可以执行的其他特权操作。

设计程序集时,应当:

标识特权代码

标识目标环境的信任级别

对高度特权代码使用 Sandbox 技术

设计公共接口

标识特权代码

标识访问受保护资源或执行安全敏感操作的代码。此类代码需要具有特定的代码访问安全权限才能运行。

标识特权资源

标识程序集需要访问的资源类型;这样,您可以识别出在程序集最终运行环境没有被授予相关代码访问安全权限的情况下可能发生的任何潜在问题。在这种情况下,您必须更新应用程序的代码访问安全策略(如果管理员允许),或者对特权代码使用 Sandbox 技术。有关 Sandbox 的详细信息,请参阅模块 9 ASP.NET 代码访问安全性。

标识特权操作

还要标识程序集需要执行的所有特权操作,以便您再次了解代码在运行时需要哪些代码访问权限。

标识目标环境的信任级别

安装程序集所在的目标环境很重要,因为代码访问安全策略可以限制允许程序集访问什么内容。例如,如果程序集依赖于 OLE DB 的使用,则该程序集在任何非完全信任环境中运行时都将失败。

完全信任环境

完全信任意味着代码具有不受限制的代码访问安全权限集,允许代码访问所有资源类型和执行特权操作,但要服从操作系统安全。完全信任环境是 Web 服务器上安装的 Web 应用程序和支持程序集的默认环境,但可以通过配置应用程序的 <trust> 元素来改变此默认设置。

部分信任环境

部分信任环境指所有非完全信任环境。.NET Framework 有几个预定义的信任级别,可以直接使用,也可以通过自定义使之满足特定的安全要求。信任级别还可能由于代码的来源而被降低。例如,网络共享上代码的信任级别就不如本地计算机上代码的信任级别高,因此它执行特权操作的能力受到了限制。

支持部分信任调用方

如果程序集支持部分信任调用方(即,不完全信任的代码),将会极大地增加安全风险。代码访问安全具有可以帮助降低风险的其他保护措施。有关适用于支持部分信任调用方的程序集的其他指南,请参阅模块 8 代码访问安全的实践。在下面两种情况下,代码无需额外编程即支持部分信任调用方:

程序集没有强名称。

程序集有强名称,但包括 AllowPartiallyTrustedCallersAttribute (APTCA) 程序集级属性。

为什么担心目标环境?

运行程序集的信任环境是非常重要的,原因如下:

部分信任程序集只能获得对有限资源集的访问权限,并且只能执行有限的一组操作,具体取决于代码访问安全策略为其授予了哪些代码访问安全权限。

部分信任程序集无法调用强名称程序集,除非它包括 AllowPartiallyTrustedCallersAttribute。

其他部分信任程序集也许不能调用您的程序集,因为它们没有必要的权限。调用程序集调用您的程序集时所需要的权限由以下因素决定:

程序集访问的资源类型

程序集执行的特权操作类型

对高度特权代码使用 Sandbox 技术

只为满足几个执行特权操作的方法的需要,可能却向整个应用程序授予了强大的权限,为了避免这种情况,可对特权代码使用 Sandbox 技术,将其放在单独的程序集中。这样,管理员可以配置代码访问安全策略,以便向特定程序集(而不是整个应用程序)中的代码授予扩展权限。

例如,如果应用程序需要调用非托管代码,可以将该非托管调用封闭在一个包装程序集中,这样管理员可以为该包装程序集而不是整个应用程序授予 UnmanagedCodePermission。

注意 Sandbox 操作需要使用一个单独的程序集,并断言安全权限以阻止完全堆栈行走。

有关对非托管 API 调用使用 Sandbox 技术的详细信息,请参阅模块 8 代码访问安全的实践中的“非托管代码”。

设计公共接口

仔细考虑使用哪些类型和成员来组成程序集的公共接口。可以通过最小化入口点数目,并使用设计良好的最小公共接口来限制程序集的攻击面。

返回页首返回页首

类设计注意事项

除了使用设计良好的最小公共接口外,还可以通过设计安全类来进一步缩小程序集的攻击面。安全类符合可靠的面向对象设计原则,可以防止不必要的继承,并限制可以调用这些类的用户和代码。下列建议有助于您设计安全类:

限制类和成员可见性

密封非基类

限制可以调用您的代码的用户

暴露使用属性的字段

限制类和成员可见性

只对组成程序集公共接口的类型及成员使用 public 访问修饰符。这立即缩小了攻击面,因为只有公共类型才可以被程序集之外的代码访问。应该尽可能地限制所有其他类型和成员。请在任何可能的地方使用 private 访问修饰符。仅当派生类可以访问成员时才使用 protected,并且仅当同一程序集中的其他类可以访问成员时才使用 internal。

注意 C# 还允许您将 protected 和 internal 结合使用,以便创建一个 protected internal 成员来限制对当前程序集或派生类的访问。

密封非基类

如果某个类没有设计为基类,可以使用 sealed 关键字来阻止继承,如下面的代码示例所示。

public sealed class NobodyDerivesFromMe{}

对于基类,可以使用代码访问安全继承请求来限制允许从您的类中派生哪些其他代码。有关详细信息,请参阅模块 8 代码访问安全的实践中的“授权代码”。

限制哪些用户可以调用代码

可以使用声明性主体权限请求批注类和方法,以控制哪些用户可以调用您的类和类成员。在下面的示例中,只有指定 Windows 组的成员可以访问 Orders 类。这种类级属性适用于所有类成员。声明性主体权限请求还可以用于单个方法。方法级属性替代了类级属性。

[PrincipalPermission(SecurityAction.Demand, Role=@"DomainName/WindowsGroup")]public sealed class Orders(){}

使用属性暴露字段

将所有字段设置为 private。要使字段值可以被外部类型访问,可以使用只读或读/写属性。还可以对属性添加其他限制,如输入验证或权限请求,如下面的代码示例所示。

public sealed class MyClass{private string field; // 字段为 private// 只有指定组的成员可以// 访问此公共属性[PrincipalPermission(SecurityAction.Demand, Role=@"DomainName/WindowsGroup")]public string Field  {get {return field;    }  }}
返回页首返回页首

强名称

程序集强名称包括文本名称、版本号、文化(可选)、公钥(常用来代表开发组织)和数字签名。可以通过查看 Machine.config 以及强名称程序集的引用方式,来查看强名称的各个组成部分。

下面的示例显示了 System.Web 程序集在 Machine.config 中是如何引用的。在此示例中,assembly 属性显示了文本名称、版本、文化和公钥令牌(公钥的简写形式)。

<add assembly="System.Web, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />

是否为程序集使用强名称取决于您打算使用程序集的方式。要为程序集添加强名称的主要原因包括:

希望确保部分信任代码无法调用您的程序集。
公共语言运行库通过为 FullTrust 权限集添加链接请求,以阻止部分信任代码调用强名称程序集。可以使用 AllowPartiallyTrustedCallersAttribute (APTCA) 替代此操作,但是使用此元素必须非常小心。

有关 APTCA 的详细信息,请参阅模块 8 代码访问安全的实践中的“APTCA”。

程序集设计为在多个应用程序之间共享。
在这种情况下,程序集应该安装在全局程序集缓存中。这需要使用强名称。全局程序集缓存支持并行版本,允许不同的应用程序绑定到同一程序集的不同版本上。

您希望使用强名称作为安全证据。
强名称的公钥部分为代码访问安全提供了加密的强证据。当您配置了代码访问安全策略以便为程序集授予特定的代码访问权限时,可以使用强名称来唯一标识程序集。加密强证据的其他形式包括验证码签名(如果您使用了 X.509 证书来签名程序集)和程序集哈希。

注意 ASP.NET 主机不加载验证码证据,这说明您不能用它来为 ASP.NET Web 应用程序建立安全策略。

有关证据类型和代码访问安全的详细信息,请参阅模块 8 代码访问安全的实践。

强名称的安全优势

强名称提供了除版本优势之外的大量安全优势:

强名称程序集使用数字签名进行签名。这可防止程序集被修改。任何篡改都会导致程序集加载时发生的验证过程失败,并会生成异常,而不会加载该程序集。

强名称程序集不能被部分信任的代码调用,除非明确添加了 AllowPartiallyTrustedCallersAttribute (APTCA)。

注意 如果您确实使用 APTCA,请务必阅读模块 8 代码访问安全的实践,以获得有关进一步提高程序集安全性的其他指南。

强名称为代码访问安全策略评估提供了加密的强证据。这允许管理员为特定程序集授予权限。还允许开发人员使用 StrongNameIdentityPermission 来限制哪些代码可以调用公共成员或从非密封类中派生。

使用强名称

.NET Framework 包括 Sn.exe 实用程序,以帮助您为程序集添加强名称。向程序集添加强名称不需要使用 X.509 证书。

向程序集添加强名称

1.

使用以下命令在程序集的项目目录中生成密钥文件。

sn.exe -k keypair.snk

2.

向 Assemblyinfo.cs 中添加 AssemblyKeyFile 属性,以引用所生成的密钥文件,如下面的代码示例中所示。

// 密钥对文件通常放在项目目录下[assembly:AssemblyKeyFile(@"../../keypair.snk")]

延迟签名

在应用程序开发过程中延迟为程序集签名是很好的安全做法。这样可以将公钥放在程序集中,以便作为证据供代码访问安全策略使用,但程序集还没有签名,因此还不能防篡改。从安全角度看,延迟签名具有两个主要优势:

用于为程序集签名并创建其数字签名的私钥被安全地存放在中心位置。该密钥只能由少数几个受信任的人使用。因此,私钥泄漏的可能性被大大减少。

单个公钥(可用来代表软件的开发组织或发行者)由开发小组中的所有成员使用,而不是每个开发人员使用自己的公钥对或私钥对(通常由 sn –k 命令生成)。

创建用于延迟签名的公钥文件

这个过程由签名机构执行,以创建公钥文件,开发人员可以使用这个公钥文件来延迟为程序集签名。

1.

为组织创建密钥对。

sn.exe-k keypair.snk

2.

将公钥从密钥对文件中提取出来。

sn-p keypair.snk publickey.snk

3.

确保 Keypair.snk 的安全,其中既包含私钥也包含公钥。例如,将它存放于软盘或 CD 上,并存放于安全的物理位置。

4.

使所有开发人员都可以使用 Publickey.snk。例如,将它放在网络共享中。

延迟为程序集签名

这个过程由开发人员执行。

1.

添加一个程序集级别的属性,以引用只包含该公钥的密钥文件。

// 密钥对文件通常放在项目目录下[assembly:AssemblyKeyFile(@"../../publickey.snk")]

2.

添加下列属性以指明延迟签名。

[assembly:AssemblyDelaySign(true)]

3.

延迟签名过程以及程序集缺少签名说明程序集在加载时将会验证失败。要解决这个问题,请在开发和测试计算机上使用下列命令。

要为特定程序集禁用验证,请使用下列命令。

sn -Vr assembly.dll

要为具有特定公钥的所有程序集禁用验证,请使用下列命令。

sn -Vr *,publickeytoken

要提取公钥和密钥令牌(公钥的截断哈希),请使用下列命令。

sn -Tp assembly.dll

注意 应使用大写的 –T 开关。

4.

要完全完成签名过程并创建数字签名,以使程序集具有防篡改功能,请执行下列命令。这需要使用私钥,因此该操作通常作为正常生成/发布过程的一部分执行。

sn-r assembly.dll keypair.snk

ASP.NET 和强名称

撰写本文的时候,还不能为 ASP.NET 网页程序集使用强名称,因为它是动态编译的。即使您使用包含代码的文件来创建包含您的页面类实现代码的预编译程序集,ASP.NET 也将动态创建并编译包含页面可视元素的类。此类派生于页面类,这再次表明您无法使用强名称。

注意 可以为任何其他由您的网页代码调用的程序集使用强名称,例如,包含资源访问、数据访问或商业逻辑代码的程序集,但是程序集必须放在全局程序集缓存中。

全局程序集缓存要求

任何由配置为部分信任的 ASP.NET Web 应用程序调用的强名称程序集都应该安装在全局程序集缓存中。这是因为 ASP.NET 主机加载所有强名称程序集并视为与域无关。

与域无关的程序集的代码可以在 ASP.NET 进程中由所有应用程序域共享。这样,如果一个强名称程序集由多个 Web 应用程序使用,并且每个应用程序为其授予不同的权限,或者所授予的权限在应用程序域重新启动之间发生变化,此时将会产生问题。在这种情况下,可以看到以下错误消息:“程序集 <assembly>.dll 安全权限授予集在应用程序域之间不兼容。”

为了避免出现这种错误,必须将强名称程序集放在全局程序集缓存中,而不要放在应用程序的 private /bin 目录下。

验证码与强名称的比较

验证码和强名称提供了程序集数字签名的两种不同方式。验证码使您可以使用 X.509 证书为程序集签名。为此,需要使用 Signcode.exe 实用程序,它将完整的 X.509 证书的公钥部分添加到程序集。这可确保证书链和证书颁发机构之间的信任。使用验证码(与强名称不同),发行者信任的实现是很复杂的,在发行者标识验证过程中将涉及到网络通信。

验证码签名和强名称用来解决不同的问题,请不要将二者混淆。尤其是:

强名称唯一标识程序集。

验证码签名唯一标识代码发行者。
验证码签名应该用于移动代码,例如,通过 Internet Explorer 下载的控件和可执行文件,以提供发行者信任和完整性。

既可以使用强名称也可以使用验证码签名来配置代码访问安全 (CAS) 策略时,以便向特定程序集授予权限。但是,从验证码签名中获得的发行者证据对象只是由 Internet Explorer 主机创建的,而不是由 ASP.NET 主机创建的。因此,在服务器端,不能使用验证码签名来标识特定程序集(通过代码组),而要使用强名称。

有关 CAS、CAS 策略和代码组的详细信息,请参阅模块 8 代码访问安全的实践。

表 7.1 比较强名称和验证码签名的功能。

表 7.1 强名称和验证码签名的比较

功能强名称验证码

程序集的唯一标识

发行者的唯一标识

不必要。程序集开发人员使用公钥代表发行者

可以吊销发行者的公钥

版本

名称空间和类型名称唯一性

完整性(检查程序集是否尚未被篡改)

用作 CAS 策略输入的证据

IE 主机 – 是。
ASP.NET 主机 – 否

信任决策所需的用户输入

是(弹出对话框)

返回页首返回页首

授权

在程序集中可以使用两种类型的授权来控制对类和类成员的访问:

基于角色的授权,根据用户标识和角色成员身份授予访问权限。在作为 ASP.NET Web 应用程序或 Web 服务的一部分的程序集中使用基于角色授权时,将为由附加在当前 Web 请求上的 IPrincipal 对象所表示的标识(可通过 Thread.CurrentPrincipal 和 HttpContext.Current.User 获得)授权。此标识或者是通过身份验证的最终用户的标识,或者是匿名 Internet 用户的标识。有关在 Web 应用程序中使用基于主体授权的详细信息,请参阅模块 10 构建安全的 ASP.NET 网页和控件中的“授权”。

代码访问安全,基于证据(如程序集的强名称或位置)为调用代码授权。有关详细信息,请参阅模块 8 代码访问安全的实践中的“授权”部分。

返回页首返回页首

异常管理

不要在返回给客户端的异常消息中泄露应用程序的实现细节。这个信息可以帮助恶意用户计划对应用程序的攻击。要提供正确的异常管理,请执行下列操作:

使用结构化异常处理

不要记录敏感数据

不要泄露系统信息或敏感的应用程序信息

考虑异常筛选器问题

考虑异常管理框架

使用结构化异常处理

Microsoft Visual C# 和 Microsoft Visual Basic .NET 提供了结构化异常处理构造。C# 提供了 try / catch 和 finally 结构。将代码放在 try 程序块内部,并实现 catch 程序块来记录和处理异常,以此来保护代码。还可以使用 finally 结构,以确保无论异常情况是否发生,都会关闭重要系统资源(如连接)。

try{// 可能生成异常的代码}catch (SomeExceptionType ex){// 处理异常并记录详细信息以帮助解决问题的代码// 问题诊断}finally{// 此代码总是运行,无论是否// 发生了异常。将清理代码放在 finally// 程序块中,以确保资源被关闭和/或释放。}

应该使用结构化异常处理,而不是从方法中返回错误代码,因为很容易忘记检查返回代码,从而导致在不安全的模式下失败。

不要记录敏感数据

Exception 对象中包括的丰富的异常细节对于开发人员和攻击者等都是很有价值的。在服务器上记录详细信息,把这些信息写到事件日志中,以帮助诊断问题。避免记录敏感或保密数据,如用户密码。还要确保异常详细信息不能传播到应用程序界限之外的客户端,如下一主题所述。

不要泄露敏感的系统或应用程序信息

不要向调用方暴露过多信息。异常详细信息可以包括操作系统、.NET Framework 版本号、方法名、计算机名、SQL 命令语句、连接字符串以及其他对攻击者非常有用的详细信息。在服务器上记录详细的错误消息,并向最终用户返回一般性错误消息。

在 ASP.NET Web 应用程序或 Web 服务的上下文中,这可以通过适当配置 <customErrors> 元素来完成。有关详细信息,请参阅模块 10 构建安全的 ASP.NET 网页和控件。

考虑异常筛选器问题

如果代码使用了异常筛选器,则容易发生安全问题,因为筛选器中的代码增加了堆栈在 finally 程序块中的代码之前可以运行的调用。确保不依赖于 finally 程序块中的状态更改,因为在异常筛选执行之前,将不会发生状态更改。例如,考虑下列代码:

// 将此代码放在 C# 类库项目中public class SomeClass{public void SomeMethod()  {try    {// (1) 生成异常Console.WriteLine("1> About to encounter an exception condition");// 模拟异常throw new Exception("Some Exception");    }// (3) finally 程序块finally    {Console.WriteLine("3> Finally");    }  }}// 将此代码放在 Visual Basic.NET 控制台应用程序项目中,// 并引用上面的类库代码Sub Main()Dim c As New SomeClassTryc.SomeMethod()Catch ex As Exception When Filter()' (4) 异常被处理Console.WriteLine("4> Main:Catch ex as Exception")End TryEnd Sub' (2) 异常筛选器Public Function Filter() As Boolean' 如果您依赖于 SomeClass 的 Finally 程序块' 的状态更改来提供安全性,恶意代码在此处将有机可乘Console.WriteLine("2> Filter")Return True ' 表明异常已处理End Function

在上面的示例中,Visual Basic .NET 用于调用 C# 类库代码,因为 Visual Basic .NET 支持异常筛选器,而 C# 不支持。

如果您创建了两个项目,然后运行这段代码,生成的输出如下所示:

1> About to encounter an exception condition2> Filter3> Finally4> Main:Catch ex as Exception

从这个输出中可以看到,异常筛选器在 finally 程序块中的代码之前执行。如果代码在 finally 程序块中设置了影响安全决策的状态,调用该代码的恶意代码会添加异常筛选器以利用这个漏洞。

考虑异常管理框架

正式的异常管理系统有助于改善系统可支持性和可维护性,并确保以一致的方式检测、记录和处理异常。

有关如何创建异常管理框架以及 .NET 应用程序异常管理最佳做法的详细信息,请参阅 MSDN Library 中的“Exception Management Architecture Guide”,其网址为:http://msdn.microsoft.com/library/en-us/dnbda/html/exceptdotnet.asp(英文)。

返回页首返回页首

文件 I/O

规范化问题是访问文件系统的代码的主要问题。如果您选择使用文件 I/O,不要让安全决策依赖于输入文件名,因为可以使用多种方式表示单个文件名。如果代码需要使用用户提供的文件名访问文件,请逐步操作以确保程序集无法被恶意用户利用来获得对敏感数据的访问或覆盖敏感数据。

下面的建议有助于您提高文件 I/O 的安全性:

避免输入不信任的文件名

不要信任环境变量

验证输入文件名

在应用程序上下文中限制文件 I/O

避免输入不信任的文件名

避免编写从调用方接受文件或路径输入的代码,相反,在读写数据时,应使用固定的文件名和位置。这可以确保您的代码无法被强迫访问任意文件。

不要信任环境变量

只要可能,尽量使用绝对文件路径。不要信任环境变量来构造文件路径,因为环境变量的值是无法保证的。

验证输入文件名

如果需要从调用方接收输入文件名,要确保文件名具有严格的格式,以便您可以确定它是否有效。特别地,输入文件路径的验证有两个方面。您需要:

检查文件系统名是否有效。

检查位置是否有效,是否如应用程序上下文中所定义的那样。例如,它们位于应用程序目录层次结构中吗?

要验证路径和文件名,请使用 System.IO.Path.GetFullPath 方法,如下面的代码示例中所示。这个方法还对所提供的文件名进行了规范化。

using System.IO;public static string ReadFile(string filename){// 获取规范化的有效文件名string name = Path.GetFullPath(filename);// 现在打开该文件}

作为规范化过程一部分,GetFullPath 执行下列检查:

它检查文件名中是否不包含任何无效字符,如 Path.InvalidPathChars 所定义。

它检查文件名是否表示一个文件,而不是其他设备类型,如物理设备、命名管道、邮件槽或 DOS 设备(如 LPT1、COM1、AUX 以及其他设备)。

它检查组合路径和文件名是否不过长。

它删除冗余字符,如尾部的点。

它拒绝使用 //?/ 格式的文件名。

在应用程序上下文中限制文件 I/O

确定具有有效的文件系统文件名之后,通常需要检查它在应用程序上下文中是否有效。例如,可能需要检查它是否位于应用程序的目录层次结构中,并确保代码无法访问文件系统上的任意文件。有关如何使用代码访问安全来限制文件 I/O 的详细信息,请参阅模块 8 代码访问安全的实践中的“文件 I/O”。

返回页首返回页首

事件日志

在编写事件日志代码时,请考虑篡改和信息暴露威胁。例如,攻击者可以通过访问事件日志检索到敏感数据吗?攻击者可以通过删除日志或清除特定记录来隐匿踪迹吗?

使用系统管理工具(如事件查看器)直接访问事件日志将受到 Windows 安全的限制。您主要应该关心的问题是,如何确保您所编写的事件日志代码不被恶意用户用来对日志记录进行未经授权的访问。

为了防止暴露敏感数据,首先请不要记录它们。例如,不要记录帐户凭据。另外,如果代码只是使用 EventLog.WriteEvent 编写新记录,则不能利用该代码来阅读现有记录或删除事件日志。这种情况下应该解决的主要威胁是,如何阻止恶意调用方在一次尝试中调用您的代码一百万次左右,从而强制日志文件循环覆盖以前的日志条目来隐匿踪迹。解决这个问题的最好办法是使用出界机制,例如,通过使用 Windows 规范,只要事件日志一达到阈值就向操作员发出警报。

最后,可以使用代码访问安全和 EventLogPermission 对代码在访问事件日志时可以执行的操作进行特定限制。例如,如果您编写的代码只需要从事件日志读取记录,则应使用只支持浏览访问的 EventLogPermissin 来限制它。有关如何限制事件日志代码的详细信息,请参阅模块 8 代码访问安全的实践中的“事件日志”。

返回页首返回页首

注册表

注册表可以提供安全位置来存储敏感的应用程序配置数据,如加密的数据库连接字符串。可以将配置数据存储在单个本机注册表项 HKEY_LOCAL_MACHINE 下,或存储在当前用户注册表项 HKEY_CURRENT_USER 下。无论使用哪一种方式,都要确保使用 DPAPI 加密数据并存储该加密数据,而不是以明文形式存放。

HKEY_LOCAL_MACHINE

如果将配置数据存储在 HKEY_LOCAL_MACHINE 下,请记住,本地计算机上的任何进程都可以潜在地访问该数据。要限制访问,应向特定注册表项应用限制性访问控制列表 (ACL),以限制对管理员和特定进程或线程令牌的访问。如果使用 HKEY_LOCAL_MACHINE,可以在安装时存储配置数据,然后在日后维护该数据,这一操作将变得非常容易。

HKEY_CURRENT_USER

如果安全要求规定使用更不容易访问的存储方案,请使用 HKEY_CURRENT_USER 的注册表项。这种方法意味着不必显式地配置 ACL,因为对当前用户注册表项的访问是根据进程标识自动限制的。

HKEY_CURRENT_USER 允许对访问使用更大的限制,因为如果加载了与当前线程或进程令牌相关的用户配置文件,进程只可以访问当前用户注册表项。

.NET Framework 的 1.1 版本在 Windows 2000 上加载 ASPNET 帐户的用户配置文件。而在 Windows Server 2003 上,只有使用 ASP.NET 进程模型时才会加载此帐户的配置文件。它不是由 Internet 信息服务 (IIS) 6 显式加载的(如果在 Windows Server 2003 上使用了 IIS 6 进程模型)。

注意 .NET Framework 的 1.0 版本不加载 ASPNET 用户配置文件,这使 HKEY_CURRENT_USER 成为一个不太常用的选项。

从注册表中读信息

下面的代码段显示了如何使用 Microsoft.Win32.Registry 类从 HKEY_CURRENT_USER 注册表项下阅读加密数据库连接串。

using Microsoft.Win32;public static string GetEncryptedConnectionString(){return (string)Registry.CurrentUser.OpenSubKey(@"SOFTWARE/YourApp").GetValue("connectionString");}

有关如何使用代码访问安全 RegistryPermission 以约束注册表访问代码(例如)以将它限制到某些特定注册表项的详细信息,请参阅模块 8 代码访问安全的实践中的“注册表”。

返回页首返回页首

数据访问

代码访问数据时,需要考虑的两个最重要的因素是,如何安全地管理数据库连接串以及如何构造 SQL 语句并验证输入以阻止 SQL 注入攻击。另外,编写数据访问代码时,考虑所选择的 ADO.NET 数据提供程序的权限要求。有关这些和其他数据访问问题的详细信息,请参阅模块 14 构建安全的数据访问。

有关如何使用 SqlClientPermission 以对使用 ADO.NET SQL Server 数据提供程序的 SQL Server 数据访问进行约束的详细信息,请参阅模块 8 代码访问安全的实践中的“数据访问”。

返回页首返回页首

非托管代码

托管代码、.NET Framework 和公共语言运行库消除了几个在非托管代码中常见的重要的安全相关漏洞。代码的类型安全验证是很好的示例,其中 .NET Framework 起了很大作用。这使托管代码的缓冲区溢出几乎成为不可能,几乎消除了基于堆栈的代码注入的威胁。

但是如果您有希望重用的现有 COM 组件或 Win32 DLL,则使用平台调用服务 (P/Invoke) 或 COM 互操作层来将它们绑定到程序集中。

调用非托管代码时,托管代码验证每个传递到非托管 API 的输入参数以防止潜在的缓冲区溢出,这是很重要的。另外,要小心处理从非托管的 API 传递回来的输出参数。

应该以独立的包装程序集方式将调用与非托管代码隔离开。这允许您砂盒高度特权代码并将代码访问安全权限要求与特定程序集隔离开。有关砂盒以及在调用非托管代码时需要应用的其他代码访问安全相关指导的详细信息,请参阅模块 8 代码访问安全的实践中的“非托管代码”。下面的建议有助于提高非托管 API 调用的安全,而不必使用显示代码访问安全编码技巧:

验证输入和输入字符串参数。

验证数组上下限。

检查文件路径长度。

使用 /GS 开关编译非托管代码。

检查非托管代码的“危险”API。

验证输入和输出字符串参数

传递到非托管 API 的字符串参数是缓冲区溢出的主要来源。检查包装程序内任何输入字符串的长度,确保它没有超过非托管 API 定义的限制。如果非托管 API 接受一个字符指针,您可能不知道它的最大权限字符串长度,除非您访问了非托管的源。例如,下面是一个常见漏洞。

void SomeFunction( char *pszInput ){char szBuffer[10];// 当心,没有作长度检查。输入被直接复制到缓冲区中// 检查长度或使用 strncpystrcpy(szBuffer, pszInput);  . . .}

如果因为没有所有权而无法检查非托管代码,请务必有意地传递长输入字符串以严格测试 API。

如果代码使用了 StringBuilder 来从非托管 API 接收字符串,请确保它能够容纳从非托管 API 传回的最长的字符串。

验证数组上下限

如果使用数组将输入传递到非托管 API,请检查托管的包装程序中作了没有超过数组容量的验证。

检查文件路径长度

如果非托管的 API 接受了文件名和路径,请检查它确实没有超过 260 字符。这个限制由 Win32 MAX_PATH 常量定义。非托管的代码分配这个长度的缓冲以操作文件路径,这是非常普遍的。

注意 目录名称和注册表项最大只能是 248 字符长。

使用 /GS 开关编译非托管代码。

如果您具有非托管代码的所有权,使用 /GS 开关来编译它以启用堆栈探测,帮助直接检测缓冲区溢出。有关 /GS 开关的详细信息,请参阅 Microsoft 知识库文章 325483,“WebCast:Compiler Security Checks:The /GS compiler switch”,其网址为:http://support.microsoft.com/default.aspx?scid=kb;EN-US;q325483(英文)。

检查非托管代码的“危险”API

如果可以访问正在调用的非托管代码的源代码,则应该对它进行一个透彻的代码检查,尤其注意参数处理以确保不可能出现缓冲区溢出,而且它没有使用潜在危险的 API。有关详细信息,请参阅模块 21 代码审查。

返回页首返回页首

委派

委派是托管的类型安全函数指针的等价物,由 .NET Framework 用来支持事件。委派对象维护着方法的引用,调用委派的时候调用这个方法。事件允许多个方法注册成为事件处理程序。当事件发生时,所有事件处理程序被调用。

不要接受来自不受信任的源的委派。

如果程序集暴露了委派或事件,您需要清楚,任何代码都可能将方法与该委派关联,您事先并不知道该代码做了什么。最安全的策略是不接受来自不信任调用方的委派。如果程序集具有强名称,且不包括 AllowPartiallyTrustedCallersAttribute,则只有完全信任调用方才可以向您传递委派。

如果程序集支持部分信任调用方,请考虑被恶意代码传递委派的其他威胁。有关解决此威胁的风险转移技巧,请参阅模块 8 代码访问安全的实践中的“委派”部分。

返回页首返回页首

序列化

如果需要按照值在 .NET 远程边界内(即,在应用程序域、进程或计算机内)整理类,或者希望能够保持对象状态以创建平数据流,这个数据流可能是为文件系统上的存储创建的,则您可能需要为类添加序列化支持。

默认情况下,类不能序列化。如果类被标记了 SerializableAttribute 或它是从 ISerializable 派生的,则该类可以被序列化。如果使用序列化:

不要对敏感数据序列化。

验证序列化数据流。

不要对敏感数据序列化

理想状况下,如果类包含敏感数据,不支持序列化。但如果必须为类序列化,且类中包含敏感数据,请避免序列化包含敏感数据的字段。为此,或者实现 ISerializable 以控制序列化行为,或者修饰包含敏感数据的、具有 [NonSerialized] 属性的字段。 默认情况下,所有 private 和 public 字段都被序列化。

下面的示例显示了如何使用 [NonSerialized] 属性以确保包含敏感数据的特定字段不被序列化。

[Serializable]public class Employee {// name 字段可以被序列化private string name;// 阻止 salary 字段被序列化[NonSerialized] private double annualSalary;  . . .}

可选地,实现 ISerializable 接口,显式地控制序列化过程。如果必须序列化敏感条目或敏感数据条目,请考虑首先为数据加密。反序列化对象的代码必须可以访问加密密钥。

验证序列化的数据流

当从序列化的数据流中创建对象实例时,不要假设该数据流包含有效数据。为避免潜在损坏注入到对象中的数据,请验证每个字段的再生,如下面的代码示例中所显示。

public void DeserializationMethod(SerializationInfo info, StreamingContext cntx){string someData = info.GetString("someName");// 使用输入验证技巧以验证数据。}

有关输入验证技巧的详细信息,请参阅模块 10 构建安全的 ASP.NET 网页和控件中的“输入验证”。

部分信任注意事项

如果代码支持部分信任调用方,则还需要解决其他威胁。例如,恶意代码可能传递序列化的数据流,或者尝试序列化对象上的数据。有关解决此威胁的风险转移技巧,请参阅模块 8 代码访问安全的实践中的“序列化”部分。

返回页首返回页首

线程

多线程状况下的竞争条件引起的缺陷可以导致安全漏洞以及易受到计时相关缺陷影响的通常不稳定的代码。如果开发多线程程序集,请考虑下列建议:

不要对安全检查的结果进行缓冲。

考虑模拟令牌。

同步静态类构造函数。

同步 Dispose 方法。

不要对安全检查的结果进行缓冲

如果多线程代码对安全检查的结果进行缓冲,也许是在静态变量中,代码则潜在地容易遭受攻击,如下面的代码示例所示。

public void AccessSecureResource()   {_callerOK = PerformSecurityDemand();OpenAndWorkWithResource();_callerOK = false;   }private void OpenAndWorkWithResource()   {if (_callerOK)PerformTrustedOperation();else     {PerformSecurityDemand();PerformTrustedOperation();     }   }

如果有其他路径进行 OpenAndWorkWithResource,而且有一个独立的线程调用同一对象上的方法,则第二个线程很有可能忽略安全命令,因为它发现其他线程已经设置了 _callerOK=true。

考虑模拟令牌

创建新线程时,假设由进程级令牌定义了安全上下文。如果父线程在创建新线程时正在模拟,则模拟令牌不会传递到新线程上。

同步静态类构造函数

如果您使用静态类构造函数,确保它们不容易受到竞争条件的影响。例如,如果它们操作静态状态,添加线程同步以避免潜在的漏洞。

同步 Dispose 方法

如果开发中使用非同步 Dispose 实现,则 Dispose 代码可能被不同的线程调用多次。下面的代码示例显示了一个这样的例子。

void Dispose(){if (null != _theObject)  {ReleaseResources(_theObject);_theObject = null;  }}

在这个示例中,在第一个线程已经将 _theObject 引用设置为 null 后,很可能两个线程都执行这段代码。根据 ReleaseResources 方法提供的功能,可能会发生安全漏洞。

返回页首返回页首

反射

使用反射,您可以动态加载程序集,发现类型信息并执行代码。还可以包含对象引用,获得或设置它的专用成员。这有许多安全意义:

如果代码使用反射来反射其他类型,请确保只有信任代码才可以调用您的代码。使用代码访问安全权限命令来为调用代码授权。有关详细信息,请参阅模块 8 代码访问安全的实践。

如果动态加载程序集,例如,使用 System.Reflection.Assembly.Load,不要使用不受信任源传递给您的程序集或类型名称。

如果程序集动态生成代码以为调用方执行操作,请确保调用方不可能影响到它生成的代码。如果调用方操作的信任级别比生成该代码的程序集的信任级别低,这个问题就更严重了。

如果代码生成依赖于调用方的输入,请务必当心安全漏洞。请验证生成的代码中用作字符串本身的所有输入字符串,并转义引号字符,以确保调用方无法打破字符串本身并注入代码。通常,如果调用方有办法影响代码生成以至于它编译失败,则很可能就存在安全漏洞。

有关详细信息,请参阅 MSDN Library 中的“Secure Coding Guidelines for the .NET Framework”(英文)。

返回页首返回页首

混淆

如果您关心如何保护知识产权,使用混淆工具,就可以使得对您程序集的 MSIL 代码使用反编译程序变得非常难。混淆工具混淆了 MSIL 指令的人工解释,有助于阻止反编译成功。

混淆并非十分坚固的,不应该用来构建依赖于混淆的安全解决方案。但是,混淆确实解决了由于代码反向工程的能力引起的威胁。混淆工具通常提供了下列优势:

它们有助于保护知识产权。

它们模糊了代码路径。攻击者很难摧毁安全逻辑。

它们混乱了内部成员变量的名称。比较难以理解代码。

它们加密字符串。攻击者经常尝试搜索特定字符串以定位关键敏感逻辑。字符串加密使得这样做更难了。

.NET Framework 有许多第三方混淆工具。Microsoft Visual Studio® .NET 2003 开发系统中包含一个工具,即,PreEmptive Solutions 提供的 Dotfuscator 的 Community Edition。它还可以从 http://www.preemptive.com/dotfuscator(英文)获得。有关详细信息,请参阅 http://www.gotdotnet.com/team/csharp/tools/default.aspx(英文)列出的 obfuscator 工具列表。

返回页首返回页首

加密

加密用来保护数据的最重要的工具之一。加密可以用来提供数据隐私和哈希算法,它可以生成固定的、浓缩的数据表示,可以用来使数据防止篡改。另外,数字签名可以用于验证目的。

如果希望数据在传输和存储过程中是安全的,应该使用加密。某些加密算法的性能比其他算法好,而有些则提供了增强加密。通常,加密密钥大小比较大可以增强安全。

使用加密过程中最常见的两个错误是:开发自己的机密算法和无法确保加密密钥的安全。加密密钥必须小心谨慎地处理。具有加密密钥的攻击者可以获得对加密数据的访问。

需要考虑的主要问题是:

使用平台提供的加密服务。

密钥生成

密钥存储

密钥交换

密钥维护

使用平台提供的加密服务

不要创建自己的加密实现。这些实现决不可能像平台(操作系统和 .NET Framework)所提供的行业标准算法那样安全。托管代码应该使用 System.Security.Cryptography 命名空间提供的算法进行加密、解密、哈希、随机数字生成和数字签名。

此命名空间中的许多类型中都包装了操作系统 CryptoAPI,而其他类型则在托管代码中实现算法。

密钥生成

下面的建议适用于创建加密密钥的时候:

生成随机密钥。

使用 PasswordDeriveBytes 进行基于密码的加密。

最好生成大密钥。

生成随机密钥

如果您需要使用编程方法生成加密密钥,使用 RNGCryptoServiceProvider 创建密钥和初始化向量,不要使用 Random 类。与 Random 类不同,RNGCryptoServiceProvider 创建与 FIPS-140 兼容的机密强随机数字。下面的代码显示了如何使用此函数。

using System.Security.Cryptography;. . .RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();byte[] key = new byte[keySize];rng.GetBytes(key);

Use PasswordDeriveBytes for Password-Based Encryption

System.Security.Cryptography.DeriveBytes 命名空间提供了 PasswordDeriveBytes,用来基于用户提供的密码来加密数据。解密时,用户必须提供加密时候使用的那个密码。

注意,这个方法不适用于密码验证。将密码验证符以具有萨尔塔值的哈希值的形式存储,以便验证用户密码。使用 PasswordDeriveBytes 生成密钥以基于密码加密。

PasswordDeriveBytes 接受密码、萨尔塔、加密算法、哈希算法、密钥大小(以位为单位)和初始化向量数据,以创建用于加密的对称密钥。

密钥用于加密数据之后,将其从内存中清除,但要保持萨尔塔和初始化向量。应该保护这些值,重新生成机密密钥时还需要使用它们。

有关存储带有萨尔塔的密码哈希的详细信息,请参阅模块 14 构建安全的数据访问。

最好使用大密钥

生成加密密钥或密钥对时,在算法中使用最大可能的密钥大小。这不见得使算法变得更安全,但是却动态地增加了对密钥暴力攻击的成功执行所需要的时间。下面的代码显示了如何找到特定算法的最大支持密钥大小。

private int GetLargestSymKeySize(SymmetricAlgorithm symAlg){KeySizes[] sizes = symAlg.LegalKeySizes;return sizes[sizes.Length].MaxSize;}private int GetLargestAsymKeySize(AsymmetricAlgorithm asymAlg){KeySizes[] sizes = asymAlg.LegalKeySizes;return sizes[sizes.Length].MaxSize;}

密钥存储

只有可能,就应该使用平台提供的加密解决方案,它能使您避免在应用程序中进行密钥管理。但有时候还需要使用要求您存储密钥的加密解决方案。将密钥存储在安全的位置,这是很关键的。使用下列技巧有助于防止密钥存储的漏洞:

使用 DPAPI 以避免密钥管理。

不要在代码中存储密钥。

限制对永久密钥的访问。

使用 DPAPI 以避免密钥管理

DPAPI 是 Microsoft Windows 2000 提供的自带的加密/解密功能,使用 DPAPI 的一个主要优势是,加密密钥由操作系统管理,因为该密钥派生于与调用 DPAPI 函数的进程帐户(或线程帐户,如果线程正在模拟)相关联的密码。

用户密钥与机器密钥的比较

执行 DPAPI 加密,可以使用用户密钥,也可以使用机器密钥。默认情况下,DPAPI 使用用户密钥。这说明只有在加密数据的用户帐户的安全上下文中运行的线程才可以解密该数据。通过将 CRYPTPROTECT_LOCAL_MACHINE 标志传递到 CryptProtectData API,可以构造 DPAPI 以使用机器密钥。在这种情况下,当前计算机上的任何用户都可以解密数据。

仅当用户执行加密的帐户已经加载了用户配置文件,才可以使用用户密钥选项。如果在没有加载用户配置文件的环境中运行代码,您无法轻易使用用户存储,则只能选择机器存储作为替代。

.NET Framework 的 1.1 版本加载 ASPNET 帐户的用户配置文件,该帐户用于在 Windows 2000 上运行 Web 应用程序。.NET Framework 的 1.0 版本不加载该用户的配置文件,这样,使用用户密钥 DPAPI 就更加困难了。

如果使用机器密钥选项,应该使用 ACL 来确保加密数据的安全,例如,在注册表项中,使用此方法来限制哪些用户可以访问机密数据。对于附加的安全,还应该将可选的熵值传递给 DPAPI 函数。

注意 熵值是附加的随机值,可以传递给 DPAPI CryptProtectData 和 CryptUnprotectData 函数。解密数据必须使用加密时候使用的那个值。机器密钥选项说明计算机上的任何用户都可以解密数据。要使用附加的熵,用户必须还知道熵值。

使用熵的缺点在于,管理密钥时还必须管理熵值。为避免熵管理问题,使用没有熵的机器存储,并在调用 DPAPI 代码之前全面验证用户和代码(使用代码访问安全)。

有关在 ASP.NET Web 应用程序中使用 DPAPI 的详细信息,请参阅“How To:Create a DPAPI Library”,位于“Building Secure ASP.NET Applications”中的“How To”部分,其网址是 http://msdn.microsoft.com/library/en-us/dnnetsec/html/SecNetHT07.asp(英文)。

不要在代码中存储密钥

不要在代码中存储密钥,因为您编译的程序集中的硬编码密钥可以使用类似 ILDASM 的工具进行反汇编,这个工具可以使您的密钥纯文本显示。

限制对永久密钥的访问

当在永久存储中存储密钥以在运行时使用的时候,应该使用适当的 ACL 并限制对密钥的访问。对密钥的访问应该只授予给 Administrators、SYSTEM 和运行时代码的标识,例如 ASPNET 或 Network Service 帐户。

备份密钥时,不要用纯文本来存储,使用 DPAPI 或强密钥进行加密,并将它放置在可移动媒体上。

密钥交换

某些应用程序要求加密密钥在非安全的网络上的安全交换。您需要口头传达密钥或者通过安全的电子邮件发送密钥。交换对称密钥的一个更安全的方法是使用公钥加密。使用这个方法,您需要通过来自可以通过验证的证书中的其他方公钥来加密要交换的对称密钥。在下列情况下,证书被认为是有效的:

在证书中指定的日期范围内使用。

证书链中的所有签名可以被验证。

类型正确。例如,电子邮件证书不应该用作 Web 服务器证书。

验证可以追溯到信任根机构。

不在颁发商的证书吊销列表 (CRL) 中。

密钥维护

是否安全取决于能否在较长的时间段中保持密钥的安全。应用下列建议进行密钥维护:

密钥周期循环。

保护导出的私钥。

密钥周期循环

您应该经常更换加密密钥,因为静态机密更可能随时间流逝而被泄漏。您将密钥记录在什么地方了吗?掌握机密的管理员 Bob 在公司中更换职位或离开公司了吗?很长时间内一直使用同一会话密钥加密通信吗?不要过分使用密钥。

密钥泄漏

密钥可以有几种泄漏方式。例如,丢失密钥,或发现攻击者已经窃取或发现了密钥。

如果用于对称加密和密钥交换的私钥被泄漏,请勿继续使用这个密钥,并通知公钥用户密钥已泄漏。如果使用这个密钥签名文档,则这些文档需要重新签名。

如果泄漏了证书的私钥,请联系颁发证书的证书颁发机构,以使证书进入证书吊销列表。另外,还要更换密钥存储的方式以避免将来再次泄漏。

保护导出的私钥

使用 PasswordDeriveBytes 导出 Rivest、Shamir 和 Adleman (RSA) 或数字签名算法 (DSA) 私钥。RSA 和 DSA 类包含一个 ToXmlString 方法,它允许您从密钥容器中导出公钥、私钥或两者。这个方法以纯文本方式导出私钥。如果将私钥导出并安装到 web 场上的多个服务器上,建议的方法是,在使用 PasswordDeriveBytes 将私钥导出后对密钥进行加密,以生成对称密钥,如下面代码示例所示。

PasswordDeriveBytes deriver = new PasswordDeriveBytes(<strong password>, null);byte[] ivZeros = new byte[8];//这实际上并不使用,但现在需要。//从密码派生密钥byte[] pbeKey = deriver.CryptDeriveKey("TripleDES", "SHA1", 192, ivZeros);
返回页首返回页首

小结

本模块已经向您显示了如何应用各种技巧以提高托管代码的安全。本模块中的技巧适用于所有类型的托管程序集,包括网页、控件、实用工具库以及其他。有关适用于特定程序集的特定建议,请参阅本指南第三部分中的其他构建模块。

要进一步提高程序集的安全,可以使用显示代码访问安全编码技巧,如果程序集支持部分信任调用方,则这些技巧尤其重要。有关使用代码访问安全的详细信息,请参阅模块 8 代码访问安全的实践。