我在实现我的第一个 Level2 SecurityCritical 程序集时学到的东西






4.54/5 (15投票s)
.NET 4.0 Level 2 安全文档的字里行间
引言
我不知道是什么驱使我尝试这个,但我决定使用 .NET 4.0 和 Level 2 安全来为独立的桌面应用程序实现一个程序集。
IDisposable
IEnumerable<T>
IEnumerable
IComparable
IEquatable
IXmlSerializable
以及继承实现这些接口的类。
这些接口的共同特征是它们都是 SecurityTransparent
。
背景
安全级别
安全级别 1 是随着 .NET Framework 2.0 引入的。它主要目的是使定位需要安全审计的代码段更加容易。它依赖于 Demand
、LinkDemand
和 Assert
。
相比之下,安全级别 2 是随着 .NET Framework 4.0 引入的,它也提供了强制执行能力。它提供了三个声明性安全层:
SecurityTransparent
SecuritySafeCritical
SecurityCritical
它还提供了在这些层之间的代码调用、继承这些层中的代码以及覆盖这些层中的代码的规则。文档对这些规则的说明相当清楚:任何层的代码都可以调用同一层或任何更不安全层的其他代码。任何层的代码也可以调用下一个更安全层的代码。这会将安全风险限制在从 SecuritySafeCritical
代码到 SecurityCritical
代码的过渡中。这些是需要安全审计的区域。
代码分析器
代码分析器可以配置为实现大量规则中的任何子集。您可以选择适合您需求的子集。
由于不确定自己的确切需求,我全力使用了代码分析器,并获得了数百个警告。在尝试仅依靠 VS 2010 文档,并辅以一些 Google 搜索来克服 Microsoft Help Viewer 的搜索限制后,我决定需要更基础的指导来领会其中的精髓。我最终找到了 Matteo Slaviero(一位 .NET 安全顾问)在 www.simple-talk.com 上撰写的以下两部分系列文章:
- .NET Framework 4.0 中代码访问安全的新特性 - 第一部分,2010 年 6 月 15 日
- .NET Framework 4.0 中代码访问安全的新特性 - 第二部分,2010 年 7 月 13 日
冒险开始……
这篇文章很有信息量且有用,但其中一些部分,例如下面这段:
本节提供的示例似乎表明,Level2 安全透明实际上是一种全有或全无的模型。如果程序集是完全受信任的,它就可以做任何事情,如果我们将其设置为SecurityTransparent
,它将无法使用受保护的资源。然而,当我们需要保护特定资源时,可以采用更细粒度的方法,这基于我们可以为程序集设置的“允许部分信任的调用者”属性(APTCA)。通过它,我们可以将代码设置为SecuritySafeCritical
,从而在SecurityTransparent
和SecurityCritical
代码之间创建桥梁。我们将在下一篇文章中详细讨论这一点。
让我产生了这样的印象:只有当程序集被标记为 AllowPartiallyTrustedCaller
属性时,SecuritySafeCritical
属性才能在程序集内部使用。事实证明并非如此。特别是,如果程序集被标记为 SecurityCritical
属性而不是 AllowPartiallyTrustedCaller
属性,您仍然可以在程序集内部使用 SecurityCritical
和 SecuritySafeCritical
属性。现在,您可能会说:“如果只能使用这些属性来提高类型、方法等的现有安全级别,那有什么用呢?”这是一个非常合理的问题,但也有一个非常合理的答案:每当您使用 C# 的 override
关键字时,默认的安全级别并不是预期的 SecurityCritical
。事实上,在这种情况下,默认是 SecurityTransparent
。您很可能会希望使用 SecurityCritical
或 SecuritySafeCritical
属性来覆盖此默认值。同样,每当您继承一个类型(例如类或结构)时,默认的安全级别是 SecurityTransparent
,您很可能希望使用这两个覆盖之一。
我遇到的最令人困扰的情况之一涉及继承的属性。每个属性都会触发一个 CA2134
警告、一个 CA2123
警告以及一系列 CA2140
警告。所有这些警告都告诉我,SomeClass.SomeInheritedProperty.get()
需要 SecurityCritical
属性。所有涉及的属性恰好都是只读的。我假设如果它们是读写属性,我也会收到关于 SomeClass.SomeInheritedProperty.set()
的警告,但我尚未验证这个假设。
我的回应是添加 SecurityCritical
属性,如下所示:
[SecurityCritical]
public override SomeType SomeInheritedProperty
{
get
{
return someValue;
}
}
这导致了以下错误:
属性 'SecurityCritical
' 对此声明类型无效。它仅对 'assembly
、class
、struct
、enum
、constructor
、method
、field
、interface
、delegate'
声明有效。
这是一个 CS0592
类型的错误,我是通过检查输出窗口而不是错误窗口来确定的(考虑到安全警告在错误窗口的警告选项卡中按类型标识,这是一个恼人的要求)。
检查此错误的文档促使我查找 SecurityCriticalAttribute
类的文档,该类提供了以下信息(SecuritySafeCriticalAttribute
类似,事实上,当该属性以这种方式使用时,情况也是如此):
[AttributeUsageAttribute(AttributeTargets.Assembly
|AttributeTargets.Class
|AttributeTargets.Struct
|AttributeTargets.Enum
|AttributeTargets.Constructor
|AttributeTargets.Method
|AttributeTargets.Field
|AttributeTargets.Interface
|AttributeTargets.Delegate,
AllowMultiple = false,
Inherited = false)]
public sealed class SecurityCriticalAttribute : Attribute
我注意到的是 AttributeTargets.Property
明显缺失。这似乎是个“两难”。经过几天的试验,我终于尝试了上面代码的这个版本:
public override SomeType SomeInheritedProperty
{
[SecurityCritical]
get
{
return someValue;
}
}
区别很微妙。我只能假设 getter 和 setter 本身都属于 AttributeTargets.Method
的控制范围。
实现 SecurityTransparent 接口
以下是一些对我有效的接口实现示例:
IDisposable
using System.Security;
...
[SecuritySafeCritical]
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!mDisposed)
{
if (disposing)
{
if (mSomeResource != null)
mSomeResource.Dispose(); // or mSomeResource.Close();,
// as appropriate
}
mSomeResource = null;
mDisposed = true;
}
}
IEnumerable<T> 和 IEnumerable
using System.Security;
...
[SecuritySafeCritical]
public IEnumerator<SomeContainedType> GetEnumerator()
{
for (int i = 0; i < Count; ++i)
{
yield return mSomeArray[i];
}
}
[SecuritySafeCritical]
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IComparable
using System.Security;
...
[SecuritySafeCritical]
public int CompareTo(ThisType other)
{
if (other == null)
throw new ArgumentNullException("other");
if (SomeProperty == other.SomeProperty)
{
if (SomeOtherProperty == other.SomeOtherProperty)
return 0;
else if (SomeOtherProperty < other.SomeOtherProperty)
return -1;
else
return 1;
}
else if (SomeProperty < other.SomeProperty)
return -1;
else
return 1;
}
IEquatable
using System.Security;
...
// Notice that I carefully avoid using "==" to
// compare instances of ThisType to each other or
// to null. This avoids infinite recursion.
// Don't go there. It's painful to debug.
[SecuritySafeCritical]
public bool Equals(ThisType other)
{
return (this.CompareTo(other) == 0);
}
[SecuritySafeCritical]
public override bool Equals(Object obj)
{
if (Object.ReferenceEquals(obj, null))
return false;
ThisType other = obj as ThisType;
if (Object.ReferenceEquals(other, null))
return false;
else
return Equals(other);
}
[SecuritySafeCritical]
public override int GetHashCode()
{
return SomeProperty.GetHashCode()
^ SomeOtherProperty.GetHashCode();
}
public static bool operator ==(ThisType instance1,
ThisType instance2)
{
if (Object.ReferenceEquals(instance1, instance2))
return true;
if (Object.ReferenceEquals(instance1, null))
return false;
if (Object.ReferenceEquals(instance2, null))
return false;
return instance1.Equals(instance2);
}
public static bool operator !=(ThisType instance1, ThisType instance2)
{
return !(instance1 == instance2);
}
public static bool operator <(ThisType instance1, ThisType instance2)
{
if (Object.ReferenceEquals(instance1, instance2))
return false;
if (Object.ReferenceEquals(instance1, null))
return false;
if (Object.ReferenceEquals(instance2, null))
return false;
return (instance1.CompareTo(instance2) < 0);
}
public static bool operator >(ThisType instance1, ThisType instance2)
{
if (Object.ReferenceEquals(instance1, instance2))
return false;
if (Object.ReferenceEquals(instance1, null))
return false;
if (Object.ReferenceEquals(instance2, null))
return false;
return (instance1.CompareTo(instance2) > 0);
}
IXmlSerializable
此接口的实现还有另一个文章的篇幅。也许以后再说。
using System.Security;
...
public override System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
[SecuritySafeCritical]
public override void ReadXml(XmlReader reader)
{
if (reader == null)
throw new ArgumentNullException("reader");
// ...
}
[SecuritySafeCritical]
public override void WriteXml(XmlWriter writer)
{
if (writer == null)
throw new ArgumentNullException("writer");
// ...
}
哎呀……
正当所有相关问题似乎都已解决时,现实来临了。虽然上述场景是实现具有程序集级别 SecurityCritical
属性的非托管应用程序所需操作的有效表示,但故事并未就此结束。
运行我的程序时,出现了错误消息:
方法 'SecurityTest.MainWindow.InitializeComponent()' 是安全透明的,但它是安全关键类型的成员。
代码分析器并未将其捕获为错误或警告。
WPF 窗口通常分为两个部分类,其中一个由编译 XAML 表示形式的窗口生成。InitializeComponent
是生成代码的一部分。我会将 SecurityCritical
属性添加到 InitializeComponent
,但没有现实的方法将安全属性添加到生成代码中。重新构建代码,至少是完整重建或修改 XAML 后重建,会替换生成代码。数据类提供了一个技巧,允许您将一个属性附加到一个类上,该属性将一个单独的类与原始类相关联。这个单独的类包含与原始类中生成的方法相对应的其他方法。这些并行方法的属性有效地应用于生成的方法。不幸的是,此机制并未为派生自 Window
的类实现。
第二种方案
此时,我决定返回使用程序集级别的 AllowPartiallyTrustedCaller
属性。我从代码中删除了所有 SecurityCritical
和 SecuritySafeCritical
属性。如果我能将 FullTrust
的 LinkDemand
限制在直接引用 FileSystemTracker
类的那个类上,大多数复杂性就会消失,所以我将那个类标记为 SecuritySafeCritical
属性,以便它能够引用 FileSystemTracker
类。
这种方法不会引入任何安全漏洞,因为代码只能以 FullTrust
的身份执行。唯一能够加载它并使用其入口点的代码也必须以 FullTrust
的身份运行。如果您在系统上运行的其他人提供的代码具有 FullTrust
,它已经可以做任何它想做的事情。使用您的入口点并不能为它带来任何新的优势。
不幸的是,这种方法会生成一系列 CA2122
警告,告诉我我的 SecuritySafeCritical
类的每个公共成员都暴露了 FileSystemWatcher
类的各种方法,而 FileSystemWatcher
具有 LinkDemand
。经过相当多的试验,我终于明白了要满足这个 LinkDemand
和代码分析器需要做什么。我添加了一个 Demand
(而不是 LinkDemand
)来请求 FullTrust
。我的 SecuritySafeCritical
类现在装饰如下:
using System.Security;
using System.Security.Permissions;
...
[SecuritySafeCritical]
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
public class MySecuritySafeCricalClass : IDisposable
{
...
}
我的代码中只有一个其他地方我不得不添加一个安全属性。我有一个 ProgressWindow
类,它使用 PlatformInvoke
来修改窗口样式。执行此操作的方法现在如下所示:
using System.Security;
...
[SecuritySafeCritical]
private void ProgressWindow_Loaded(object sender, RoutedEventArgs e)
{
if (!mLoaded)
{
mLoaded = true;
IntPtr hWnd = new WindowInteropHelper(this).Handle;
SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & ~WS_SYSMENU);
}
}
关注点
在此过程中,我发现了许多关于尝试将尽可能多的代码标记为 internal
,并将程序集标记为 AllowPartiallyTrustedCaller
的事情。我发现这种情况下的这种方法是无用的:实现公共接口的所有类都需要是公共的。FileSystemWatcher
的使用无法通过将所有内容设为 internal
来限制。如果您的 App
类是使用 XAML 实现的,您不能将您的启动窗口设为 internal
。生活就是如此。然而,如上文“第二种方案”所示,通过结合使用 SecuritySafeCritical 和 PermissionSet 属性,可以限制 FileSystemWatcher
的使用。
结论
尽管 Level 2 安全性比 Level 1 安全性有了很大改进,但实施它并非没有陷阱。文档还有很大的改进空间。我希望本文能帮助您轻松上手,尤其是在开发设计为完全信任运行的桌面应用程序时。我认为这次练习也让我对 Level 2 安全性有了更深入的了解,并使其更容易实现设计为在 SecurityTransparent
环境或 AllowPartiallyTrustedCaller
环境中运行的托管程序集。
历史
- 2012 年 2 月 18 日:“背景”部分已扩展。“哎呀……”和“第二种方案”部分已添加。
- 2012 年 2 月 14 日:提交原始版本。