数据的安全注意事项

在 Windows Communication Foundation(WCF)中处理数据时,必须考虑多种威胁类别。 以下列表显示了与数据处理相关的最重要的威胁类。 WCF 提供了缓解这些威胁的工具。

  • 拒绝服务

    当接收不受信任的数据时,数据可能会导致接收方通过导致长时间计算来访问不成比例的各种资源,例如内存、线程、可用连接或处理器周期。 针对服务器的拒绝服务攻击可能会导致服务器崩溃,并且无法处理来自其他合法客户端的消息。

  • 恶意代码执行

    传入不受信任的数据会导致接收方运行它不打算的代码。

  • 信息泄露

    远程攻击者强制接收方以比接收方打算披露更多信息的方式响应其请求。

用户提供的代码和代码访问安全性

Windows Communication Foundation(WCF)基础结构中的许多位置运行由用户提供的代码。 例如, DataContractSerializer 序列化引擎可能调用用户提供的属性 set 访问器和 get 访问器。 WCF 通道基础结构还可以调用用户提供的 Message 类的派生类。

代码作者有责任确保不存在安全漏洞。 例如,如果使用类型为整数的数据成员属性创建数据协定类型,并在 set 访问器实现中基于属性值分配数组,则如果恶意消息包含此数据成员的极大值,则可能会公开拒绝服务攻击的可能性。 一般情况下,请避免基于传入数据或用户提供代码中长时间处理的任何分配(特别是如果长时间处理可能是由少量传入数据引起的)。 对用户提供的代码执行安全分析时,请确保还要考虑所有失败情况(即引发异常的所有代码分支)。

由用户提供的代码的最佳示例是服务中每个操作的实现代码。 服务实现的安全性由你负责。 很容易无意中创建可能导致拒绝服务漏洞的不安全作实现。 例如,一个操作是接受一个字符串并从数据库中返回客户列表,这些客户的名字以该字符串开头。 如果使用的是大型数据库,并且传递的字符串只是一个字母,则代码可能会尝试创建大于所有可用内存的消息,从而导致整个服务失败。 在 .NET Framework 中,OutOfMemoryException 是不可恢复的,并且始终会导致应用程序的终止。

应确保没有恶意代码插入到各种扩展点。 当在部分信任下运行、处理部分受信任的程序集中的类型或创建可由部分受信任的代码使用的组件时,这尤其相关。 有关详细信息,请参阅后面的部分中的“部分信任威胁”。

请注意,在部分信任中运行时,数据协定序列化基础结构仅支持数据协定编程模型的有限子集,例如,不支持使用该属性的 SerializableAttribute 私有数据成员或类型。 有关详细信息,请参阅 部分信任

注释

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

避免无意中的信息泄露

在设计具有安全性的可序列化类型时,信息泄露是一个可能的问题。

请考虑以下几点:

  • 编程 DataContractSerializer 模型允许在序列化期间在类型或程序集之外公开私有数据和内部数据。 此外,可以在架构导出期间公开类型的形状。 请务必了解类型的序列化工程。 如果不希望公开任何内容,请禁用序列化它(例如,在数据协定的情况下不应用 DataMemberAttribute 属性)。

  • 请注意,同一类型可能有多个序列化投影,具体取决于正在使用的序列化程序。 同一类型在与DataContractSerializer一起使用时可能会暴露一组数据,在与XmlSerializer一起使用时则可能会暴露另一组数据。 意外使用错误的序列化程序可能会导致信息泄露。

  • 在旧式远程过程调用(RPC)/编码模式中使用XmlSerializer,可能会无意中暴露发送端的对象图形状给接收端。

防止拒绝服务攻击

配额

导致接收方分配大量内存是潜在的拒绝服务攻击。 虽然本部分侧重于大型消息产生的内存消耗问题,但可能会发生其他攻击。 例如,消息可能会使用不成比例的处理时间。

拒绝服务攻击通常使用配额进行缓解。 当超过配额时,通常会引发 QuotaExceededException 异常。 如果没有配额,恶意消息可能会导致所有可用内存被访问,从而导致OutOfMemoryException异常,或导致所有可用堆栈被访问,从而导致StackOverflowException异常。

超出配额的方案是可恢复的;如果在正在运行的服务中遇到,则丢弃当前正在处理的消息,并且服务会继续运行并处理其他消息。 但是,内存不足和堆栈溢出方案在 .NET Framework 中的任何位置都不可恢复;如果服务遇到此类异常,服务将终止。

WCF 中的配额不涉及任何预分配。 例如,如果 MaxReceivedMessageSize 配额(在各种类上找到)设置为 128 KB,则并不意味着为每个消息自动分配 128 KB。 分配的实际金额取决于实际传入消息大小。

传输层有许多配额。 这些配额由正在使用的特定传输通道(HTTP、TCP 等)强制实施。 虽然本主题讨论其中一些配额,但这些配额在 传输配额中进行了详细介绍。

哈希表漏洞

当数据协定包含哈希表或集合时存在漏洞。 如果将大量值插入到哈希表中,其中大量值生成相同的哈希值,则会出现此问题。 这可用作 DOS 攻击。 可以通过设置 MaxReceivedMessageSize 绑定配额来缓解此漏洞。 若要防止此类攻击,必须注意设置此配额。 此配额对 WCF 消息的大小施加上限。 此外,避免在数据协定中使用哈希表或集合。

在不流式处理的情况下限制内存消耗

防止消息过大的安全模型取决于是否使用流模式。 在基本的非流式处理情况下,消息将缓冲到内存中。 在这种情况下,请在 MaxReceivedMessageSize 上或系统提供的绑定上使用 TransportBindingElement 配额,通过限制可访问的最大消息大小来防范大型消息。 请注意,服务可能同时处理多个消息,在这种情况下,它们都在内存中。 使用遏制功能可缓解这种威胁。

另请注意, MaxReceivedMessageSize 不对每条消息内存消耗施加上限,而是将其限制在常量因子内。 例如,如果 MaxReceivedMessageSize 是 1 MB,并且接收到 1 MB 的消息然后反序列化,则需要额外的内存来包含反序列化的对象图形,这会导致总内存消耗量大大超过 1 MB。 因此,请避免创建可序列化类型,这些类型可能会导致大量内存消耗,而无需大量传入数据。 例如,一个具有 50 个可选数据成员字段和 100 个专用字段的数据契约“MyContract”,可以通过 XML 构造“<MyContract/>”实例化。 此 XML 将导致为 150 个字段访问内存。 请注意,默认情况下,数据成员是可选的。 当此类类型是数组的一部分时,问题会复杂化。

MaxReceivedMessageSize 仅此一项不足以防止所有拒绝服务攻击。 例如,反序列化程序可能被迫通过接收到的消息来反序列化一个深度嵌套的对象图(即一个对象包含另一个对象,而后者又包含另一个对象,依此类推)。 DataContractSerializerXmlSerializer 都以嵌套的方式调用方法来反序列化此类图形。 方法调用的深度嵌套可能导致不可恢复的 StackOverflowException。 通过将配额设置为 MaxDepth 限制 XML 嵌套级别来缓解此威胁,如本主题后面的“使用 XML 安全”部分中所述。

使用二进制 XML 编码时,将其他配额设置为 MaxReceivedMessageSize 特别重要。 使用二进制编码与压缩有点等效:传入消息中的一小组字节可能表示大量数据。 因此,即使是符合MaxReceivedMessageSize限制的消息,在完全展开后可能会占用更多的内存。 若要缓解此类特定于 XML 的威胁,必须正确设置所有 XML 读取器配额,如本主题后面的“使用 XML 安全”部分所述。

在使用流模式的情况下限制内存消耗

流媒体时,可以使用一个小 MaxReceivedMessageSize 设置来防范拒绝服务攻击。 但是,在流模式下可能出现更复杂的情况。 例如,文件上传服务接受大于所有可用内存的文件。 在这种情况下,请将 MaxReceivedMessageSize 该值设置为非常大的值,预期内存中几乎没有数据缓冲,消息流直接缓冲到磁盘。 如果恶意消息可以某种方式强制 WCF 缓冲数据,而不是在这种情况下对其进行流式处理, MaxReceivedMessageSize 则不再防止访问所有可用内存的消息。

为了缓解此威胁,限制缓冲的各种 WCF 数据处理组件上存在特定的配额设置。 在这些中,MaxBufferSize 属性是各种传输绑定元素和标准绑定中最重要的一个。 流式处理时,应根据你愿意为每个消息分配的最大内存量来设定此配额。 与上 MaxReceivedMessageSize一样,该设置不会对内存消耗施加绝对最大值,但只会将其限制在一个常量因子内。 此外,与 MaxReceivedMessageSize一样,也要注意同时处理多个消息的可能性。

MaxBufferSize 详细信息

MaxBufferSize 属性限制 WCF 执行的大容量缓冲操作。 例如,WCF 始终缓冲 SOAP 标头和 SOAP 错误,并且对于在消息传输优化机制(MTOM)消息中发现不按自然读取顺序排列的任何 MIME 部件,亦会进行缓冲处理。 此设置限制所有这些情况下的缓冲量。

WCF 通过将 MaxBufferSize 的值传递给可能缓冲的各种组件来实现这一目标。 例如, CreateMessage 类的某些 Message 重载采用参数 maxSizeOfHeaders 。 WCF 将 MaxBufferSize 值传递给此参数以限制 SOAP 标头缓冲量。 直接使用 Message 类时,必须设置此参数。 通常,在 WCF 中使用采用配额参数的组件时,必须了解这些参数的安全影响并正确设置它们。

MTOM 消息编码器还有一个 MaxBufferSize 设置。 使用标准绑定时,会自动将此值设置为传输级别 MaxBufferSize 值。 但是,使用 MTOM 消息编码器绑定元素构造自定义绑定时,在使用流式处理时,请务必将 MaxBufferSize 属性设置为安全值。

基于 XML 的流式攻击

MaxBufferSize 单独不足以确保 WCF 在预期流式处理时无法强制进入缓冲。 例如,WCF XML 读取器在开始读取新元素时始终缓冲整个 XML 元素开始标记。 这样做是为了正确处理命名空间和属性。 如果 MaxReceivedMessageSize 配置为较大(例如,若要启用直接到磁盘的大型文件流式处理方案),则可能会构造恶意消息,其中整个消息正文是大型 XML 元素启动标记。 尝试读取它会导致 OutOfMemoryException。 这是许多可能的基于 XML 的拒绝服务攻击之一,这些攻击都可以使用 XML 读取器配额来缓解,本主题后面的“使用 XML 安全”部分进行了讨论。 当使用流模式时,设置所有这些配额尤为重要。

混合流式和缓冲编程模型

许多可能的攻击源于在同一服务中混合流式处理和非流式编程模型。 假定存在一个具有两个操作的服务协定:一个操作采用一个 Stream ,另一个操作采用某个自定义类型的数组。 假设还将 MaxReceivedMessageSize 设置为一个大值,以便第一个操作能够处理大规模数据流。 不幸的是,这意味着现在可以将大型消息也发送到第二个操作,并且在调用操作之前,反序列化程序会将内存中的数据缓冲为数组。 这是潜在的拒绝服务攻击: MaxBufferSize 配额不限制反序列化程序所处理的消息正文的大小。

因此,请避免在同一合约中混合基于流的和非流的操作。 如果绝对必须混合这两种编程模型,请使用以下预防措施:

当非流式操作使用 DataContractSerializer 时,上述预防措施适用。 在使用 XmlSerializer 时,如果流式处理和非流式处理编程模型共用同一服务,请避免混合使用,因为它没有 MaxItemsInObjectGraph 配额的保护。

慢流攻击

一类流式拒绝服务攻击不涉及内存消耗。 此类攻击涉及数据的慢速发送方或接收方。 等待发送或接收数据时,线程和可用连接等资源已用尽。 恶意攻击或者慢速网络连接上的合法发送方/接收方均会导致这种情形的发生。

若要缓解这些攻击,请正确设置传输超时。 有关详细信息,请参阅 传输配额。 其次,在 WCF 中使用流时,切勿使用同步 ReadWrite 操作。

安全地使用 XML

注释

虽然本部分与 XML 有关,但信息也适用于 JavaScript 对象表示法(JSON)文档。 配额的工作方式类似,使用 JSON 和 XML 之间的映射

保护 XML 读取器

XML Infoset 构成了 WCF 中所有消息处理的基础。 从不受信任的源接受 XML 数据时,必须缓解许多拒绝服务攻击的可能性。 WCF 提供特殊的安全 XML 读取器。 在 WCF 中使用标准编码之一(文本、二进制或 MTOM)时,会自动创建这些读取器。

这些读取器上的一些安全功能始终处于活动状态。 例如,读取者从不处理文档类型定义(DTD),这是拒绝服务攻击的潜在来源,不应出现在合法的 SOAP 消息中。 其他安全功能包括必须配置的读取器配额,如以下部分所述。

当直接使用 XML 读取器(例如编写自己的自定义编码器或直接处理 Message 类时),当有机会使用不受信任的数据时,始终使用 WCF 安全读取器。 通过在CreateTextReader类调用CreateBinaryReaderCreateMtomReaderXmlDictionaryReader的其中一个静态工厂方法重载来创建一个安全读取器。 创建读取器时,传入安全配额值。 不要调用 Create 方法重载。 这些不会创建 WCF 读取器。 而是创建一个不受本节中所述安全功能保护的读取器。

读取器配额

安全 XML 读取器有五个可配置配额。 这些属性通常使用 ReaderQuotas 编码绑定元素或标准绑定上的属性进行配置,或者通过使用 XmlDictionaryReaderQuotas 创建读取器时传递的对象进行配置。

MaxBytesPerRead

此配额限制读取元素开始标记及其属性时在单个 Read 操作中读取的字节数。 (在非流式处理的情况下,元素名称本身不计入配额。 MaxBytesPerRead 对于以下原因非常重要:

  • 读取元素名称及其属性时,它们始终缓存在内存中。 因此,请务必在流模式下正确设置此配额,以防止在预期流式处理时过度缓冲。 有关缓冲的实际数量的信息,请参阅MaxDepth配额章节。

  • 具有过多的 XML 属性可能会占用不成比例的处理时间,因为必须检查属性名称的唯一性。 MaxBytesPerRead 可缓解这一威胁。

最大深度 (MaxDepth)

此配额限制 XML 元素的最大嵌套深度。 例如,文档“<A><B><C//><B></A>”具有三个嵌套深度。 MaxDepth 对于以下原因非常重要:

  • MaxDepthMaxBytesPerRead 交互时:读取器始终将数据保留在当前元素及其所有祖先的内存中,因此读取器的最大内存消耗与这两个设置的乘积成比例。

  • 当反序列化深度嵌套的对象图时,会迫使反序列化程序访问整个堆栈并引发一个不可恢复的 StackOverflowException。 XML 嵌套和对象嵌套DataContractSerializerXmlSerializer之间存在直接关联。 用 MaxDepth 缓解此威胁。

MaxNameTableCharCount

此配额限制读取器的名称表 的大小。 nametable 包含处理 XML 文档时遇到的某些字符串(如命名空间和前缀)。 由于这些字符串在内存中缓冲,请设置此配额以防止在预期流式处理时过度缓冲。

最大字符串内容长度

此配额限制 XML 读取器返回的最大字符串大小。 此配额不会限制 XML 读取器本身的内存消耗,而是限制正在使用读取器的组件的内存消耗。 例如,当DataContractSerializer使用受保护的MaxStringContentLength读取器时,它不会反序列化大于此配额的字符串。 直接使用 XmlDictionaryReader 类时,并非所有方法都遵循此配额,只有专门用于读取字符串的方法,例如 ReadContentAsString 方法,才会遵循。 Value读取器上的属性不受此配额的影响,因此在需要此配额提供保护的情况下不应使用该属性。

MaxArrayLength

此配额限制 XML 读取器返回的基元数组的最大大小,包括字节数组。 此配额不会限制 XML 读取器本身的内存消耗,而是限制使用读取器的任何组件。 例如,当DataContractSerializer使用受MaxArrayLength保护的读取器时,它不会反序列化大于此配额的字节数组。 尝试在单个协定中混合流式处理和缓冲编程模型时,必须设置此配额。 请记住,直接使用 XmlDictionaryReader 类时,只有专门用于读取某些基元类型的任意大小的数组的方法(例如 ReadInt32Array,遵循此配额)。

特定于二进制编码的威胁

WCF 支持的二进制 XML 编码包括字典字符串功能。 只能使用几个字节对大型字符串进行编码。 这可实现显著的性能提升,但引入了必须缓解的新拒绝服务威胁。

有两种类型的字典: 静态动态。 静态字典是一个内置的长字符串列表,可以使用二进制编码中的短代码表示。 创建读取器且无法修改时,此字符串列表是固定的。 默认情况下 WCF 使用的静态字典中没有一个字符串足够大,无法构成严重的拒绝服务威胁,尽管它们可能仍在字典扩展攻击中使用。 在提供自己的静态字典的高级方案中,在引入大型字典字符串时要小心。

动态字典功能允许消息定义自己的字符串,并将其与短代码相关联。 这些字符串到代码映射在整个通信会话期间保留在内存中,这样后续消息就不必重新发送字符串,并且可以利用已定义的代码。 这些字符串可能具有任意长度,因此比静态字典中的字符串构成更严重的威胁。

必须缓解的第一个威胁是动态字典(字符串到代码映射表)变得太大的可能性。 此字典可能会在多个消息过程中扩展,因此 MaxReceivedMessageSize 配额不提供保护,因为它仅适用于每个消息。 因此,MaxSessionSize上存在一个单独的BinaryMessageEncodingBindingElement属性,用于限制字典的大小。

与其他大多数配额不同,在写入消息时,此配额也适用。 如果在读取消息时超过了限制,QuotaExceededException 会像往常一样被抛出。 如果在编写消息时超出此限制,则任何导致超出配额的字符串都会 as-is写入,而无需使用动态字典功能。

字典展开威胁

字典扩展引发大量特定于二进制的攻击。 一个小的二进制形式的消息如果大量使用了字符串字典功能,那么它的完全展开的文本形式可能是一个非常大的消息。 动态字典字符串的扩展因子受 MaxSessionSize 配额限制,因为没有动态字典字符串超过整个字典的最大大小。

MaxNameTableCharCountMaxStringContentLengthMaxArrayLength属性仅限制内存消耗。 通常不需要它们来缓解非流式处理使用情况中的任何威胁,因为内存使用量已受到 MaxReceivedMessageSize限制。 但是, MaxReceivedMessageSize 计算预扩展字节数。 使用二进制编码时,内存消耗可能会超出 MaxReceivedMessageSize范围,仅受以下因素 MaxSessionSize的限制。 因此,在使用二进制编码时,请务必始终设置所有读取器配额(尤其是 MaxStringContentLength)。

在结合使用二进制编码和 DataContractSerializer 时,可能会误用 IExtensibleDataObject 接口来发动字典扩展攻击。 此接口实质上为不属于协定的任意数据提供无限制的存储。 如果配额不能设置得足够低以使 MaxSessionSizeMaxReceivedMessageSize 的乘积不会造成问题,那么当使用二进制编码时应禁用 IExtensibleDataObject 功能。 将 IgnoreExtensionDataObject 属性的值设置为 trueServiceBehaviorAttribute 属性上。 或者,不要实现 IExtensibleDataObject 接口。 有关详细信息,请参阅 Forward-Compatible 数据协定

配额概述

下表总结了有关配额的指导。

条件 要设置的重要配额
非流式或流式小型消息、文本或 MTOM 编码 MaxReceivedMessageSizeMaxBytesPerReadMaxDepth
非流式或流式小型消息或二进制编码 MaxReceivedMessageSizeMaxSessionSize 和所有 ReaderQuotas
流式大型消息、文本或 MTOM 编码 MaxBufferSize 和所有 ReaderQuotas
流式大型消息或二进制编码 MaxBufferSizeMaxSessionSize 和所有 ReaderQuotas
  • 必须总是设置传输层超时值,并且当使用流模式时切勿使用同步读/写,无论您是对大消息还是小消息进行流式处理,都是如此。

  • 怀疑配额时,将其设置为安全值,而不是将其保持打开状态。

防止恶意代码执行

以下一般威胁类可以执行代码并产生意外的影响:

  • 反序列化程序加载恶意的、不安全的或者安全性敏感类型。

  • 传入消息导致反序列化程序以一种具有意外后果的方式构造一个通常而言很安全的类型的实例。

以下各节进一步讨论这些威胁类。

DataContractSerializer

(有关XmlSerializer的安全信息,请参阅相关文档。)XmlSerializer的安全模型与DataContractSerializer的安全模型类似,区别主要在于细节。 例如,该 XmlIncludeAttribute 属性用于类型包含,而不是 KnownTypeAttribute 属性。 但是, XmlSerializer 特有的一些威胁将在本主题的后面部分讨论。

防止加载意外类型

加载非预期类型可能会产生重大后果,无论这些类型是恶意的还是仅具有安全敏感的副作用。 类型可能包含可利用的安全漏洞、在其构造函数或类构造函数中执行安全敏感作、具有有助于拒绝服务攻击的大型内存占用量,或者可能会引发不可恢复的异常。 类型可能具有类构造函数,这些构造函数在加载类型后以及创建任何实例之前运行。 出于这些原因,必须控制反序列化程序可能加载的类型集。

DataContractSerializer 以一种松散耦合的方式进行反序列化。 它永远不会从传入数据读取公共语言运行时(CLR)类型和程序集名称。 这与XmlSerializer行为相似,但与NetDataContractSerializerBinaryFormatter以及SoapFormatter的行为不同。 松散耦合会引入一定程度的安全性,因为远程攻击者无法通过在消息中命名某个类型来指示首先加载该类型。

始终允许 DataContractSerializer 加载按照协定当前预计加载的类型。 例如,如果数据协定具有Customer类型的数据成员,则DataContractSerializer允许在反序列化此数据成员时加载Customer类型。

此外,DataContractSerializer 还支持多态性。 数据成员可以声明为 Object,但传入的数据可能包含实例 Customer 。 仅当通过下列机制之一使Customer类型对反序列化程序变得“已知”,才有可能执行这种操作。

  • 应用于类型的 KnownTypeAttribute 属性。

  • KnownTypeAttribute 属性指定返回类型列表的方法。

  • ServiceKnownTypeAttribute 属性。

  • KnownTypes 配置节。

  • 在构造期间向 DataContractSerializer 显式传递的已知类型列表(如果直接使用序列化程序)。

上述每个机制都引入了反序列化程序可加载的更多类型,从而增加了外围应用。 控制每个机制,以确保不会将恶意或意外类型添加到已知类型列表中。

已知类型处于范围内后,可以随时加载它,并且可以创建该类型的实例,即使协定实际上禁止使用它。 例如,假设使用上述机制之一将类型“MyDangerousType”添加到已知类型列表中。 这意味着:

  • MyDangerousType 加载并运行其类构造函数。

  • 即使当反序列化具有一个字符串数据成员的数据协定时,恶意消息也可能导致创建 MyDangerousType 的一个实例。 在MyDangerousType中的代码(例如属性设置器)可能会运行。 完成此作后,反序列化程序会尝试将此实例分配给字符串数据成员,并失败并出现异常。

编写返回已知类型列表的方法或将列表直接 DataContractSerializer 传递给构造函数时,请确保准备列表的代码是安全的,并且仅对受信任的数据进行作。

如果在配置中指定已知类型,请确保配置文件是安全的。 始终在配置中使用强名称(通过指定类型所在的已签名程序集的公钥),但不指定要加载的类型版本。 如果可能,类型加载程序会自动选取最新版本。 如果在配置中指定了特定版本,您将面临以下风险:某个类型可能存在安全漏洞,并可能在未来版本中得到修复,但因为在配置中被显式指定,易受攻击的版本仍会被加载。

具有过多的已知类型会产生另一个后果:在 DataContractSerializer 应用程序域中创建序列化/反序列化代码的缓存,每个类型的条目必须序列化和反序列化。 只要应用程序域正在运行,就不会清除此缓存。 因此,知道应用程序使用许多已知类型的攻击者可能会导致所有这些类型的反序列化,从而导致缓存占用过多的内存。

防止类型处于意外状态

类型可能具有必须强制执行的内部一致性约束。 必须注意避免在反序列化期间打破这些约束。

下面的类型示例表示航天器上空锁的状态,并强制约束内门和外门不能同时打开。

[DataContract]
public class SpaceStationAirlock
{
    [DataMember]
    private bool innerDoorOpenValue = false;
    [DataMember]
    private bool outerDoorOpenValue = false;

    public bool InnerDoorOpen
    {
        get { return innerDoorOpenValue; }
        set
        {
            if (value & outerDoorOpenValue)
                throw new Exception("Cannot open both doors!");
            else innerDoorOpenValue = value;
        }
    }
    public bool OuterDoorOpen
    {
        get { return outerDoorOpenValue; }
        set
        {
            if (value & innerDoorOpenValue)
                throw new Exception("Cannot open both doors!");
            else outerDoorOpenValue = value;
        }
    }
}
<DataContract()> _
Public Class SpaceStationAirlock
    <DataMember()> Private innerDoorOpenValue As Boolean = False
    <DataMember()> Private outerDoorOpenValue As Boolean = False

    Public Property InnerDoorOpen() As Boolean
        Get

            Return innerDoorOpenValue
        End Get
        Set(ByVal value As Boolean)
            If (value & outerDoorOpenValue) Then
                Throw New Exception("Cannot open both doors!")
            Else
                innerDoorOpenValue = value
            End If
        End Set
    End Property

    Public Property OuterDoorOpen() As Boolean
        Get
            Return outerDoorOpenValue
        End Get
        Set(ByVal value As Boolean)
            If (value & innerDoorOpenValue) Then
                Throw New Exception("Cannot open both doors!")
            Else
                outerDoorOpenValue = value
            End If
        End Set
    End Property
End Class

攻击者可能会发送如下所示的恶意消息,绕过约束,使对象进入无效状态,这可能会产生意外和不可预知的后果。

<SpaceStationAirlock>
    <innerDoorOpen>true</innerDoorOpen>
    <outerDoorOpen>true</outerDoorOpen>
</SpaceStationAirlock>

可以通过注意以下几点来避免这种情况:

  • DataContractSerializer反序列化大多数类时,构造函数不会运行。 因此,不要依赖于在构造函数中完成的任何状态管理。

  • 使用回调来确保对象处于有效状态。 用特性标记的 OnDeserializedAttribute 回调特别有用,因为它在反序列化完成后运行,并有机会检查和更正总体状态。 有关详细信息,请参阅 Version-Tolerant 序列化回调

  • 在设计数据协定类型时,不要使它依赖项属性 setter 的任何特定调用顺序。

  • 请小心使用标有 SerializableAttribute 属性的旧类型。 它们中的许多都旨在使用 .NET Framework 远程处理以便仅用于受信任的数据。 使用此属性标记的现有类型可能尚未考虑到状态安全。

  • 考虑到状态安全性,不要依赖 IsRequired 属性 (Attribute) 的 DataMemberAttribute 属性 (Property) 来保证数据的存在。 数据始终可以是 nullzeroinvalid

  • 永远不要信任从不受信任的数据源反序列化的对象图,除非先对其进行验证。 每个对象可能处于一致状态,但整个对象图可能不是。 此外,即使禁用了对象图形保留模式,反序列化图形也可能有多个对同一对象的引用或具有循环引用。 有关详细信息,请参阅 序列化和反序列化

安全地使用 NetDataContractSerializer

NetDataContractSerializer 是一个使用与类型紧密耦合的序列化引擎。 这类似于 BinaryFormatterSoapFormatter. 也就是说,它通过从传入数据读取 .NET Framework 程序集和类型名称来确定要实例化的类型。 虽然它是 WCF 的一部分,但没有提供插入此序列化引擎的方法;必须编写自定义代码。 提供 NetDataContractSerializer 主要是为了简化从 .NET Framework 远程处理到 WCF 的迁移过程。 有关详细信息,请参阅 序列化和反序列化中的相关部分。

由于消息本身可能指示可以加载任何类型,因此机制 NetDataContractSerializer 本质上不安全,并且只应与受信任的数据一起使用。 有关详细信息,请参阅 BinaryFormatter 安全指南

即使与受信任的数据一起使用,传入的数据也可能无法指定要加载的类型,尤其是在属性 AssemblyFormat 设置为 Simple 时。 有权访问应用程序的目录或全局程序集缓存的任何人都可以替代恶意类型来代替应加载的类型。 始终通过正确设置权限来确保应用程序的目录和全局程序集缓存的安全性。

通常,如果允许部分受信任的代码访问 NetDataContractSerializer 实例,或者控制代理项选择器(ISurrogateSelector)或序列化联编程序(SerializationBinder),则代码可能会对序列化/反序列化过程执行大量控制。 例如,它可以注入任意类型、导致信息泄露、篡改生成的对象图或序列化数据,或溢出生成的序列化流。

另一个与 NetDataContractSerializer 相关的安全问题是拒绝服务,而不是恶意代码执行威胁。 当使用 NetDataContractSerializer 时,请始终将 MaxItemsInObjectGraph 配额设置为安全值。 很容易构造一条小型恶意消息,用于分配一个对象数组,其大小仅受此配额限制。

XmlSerializer 特有的威胁

XmlSerializer安全模型类似于此DataContractSerializer模型。 然而,有一些威胁是XmlSerializer所特有的。

XmlSerializer 运行时生成 序列化程序集 ,其中包含实际序列化和反序列化的代码;这些程序集是在临时文件目录中创建的。 如果某些其他进程或用户有权访问该目录,他们可能会使用任意代码覆盖序列化/反序列化代码。 XmlSerializer然后,使用其安全上下文运行此代码,而不是序列化/反序列化代码。 确保对临时文件目录正确设置权限,以防止这种情况发生。

XmlSerializer它还具有一种模式,在该模式下,它使用预生成的序列化程序集,而不是在运行时生成它们。 每当 XmlSerializer 可以找到合适的序列化程序集时,就会触发此模式。 XmlSerializer 检查对序列化程序集进行签名的密钥是否就是对包含所序列化的类型的程序集进行签名的密钥。 这可提供保护,防止恶意程序集伪装为序列化程序集。 但是,如果包含可序列化类型的程序集未签名,则 XmlSerializer 无法执行此检查并使用具有正确名称的任何程序集。 这使得运行恶意代码成为可能。 始终对包含可序列化类型的程序集进行签名,或严格控制对应用程序的目录和全局程序集缓存的访问,以防止引入恶意程序集。

XmlSerializer可能会受到拒绝服务攻击。 XmlSerializer 没有 MaxItemsInObjectGraph 所具有的 DataContractSerializer配额。 因此,它反序列化任意数量的对象,仅受消息大小限制。

部分信任威胁

请注意以下有关与使用部分信任运行的代码相关的威胁的问题。 这些威胁包括恶意的部分信任代码,以及恶意的部分信任代码与其他攻击场景的结合(例如,部分信任代码构造特定字符串,然后对其进行反序列化)。

  • 使用任何序列化组件时,切勿在进行这样的使用之前断言任何权限,即使整个序列化方案是在断言的范围之内,并且您未处理任何不受信任的数据或对象,也不要进行这样的断言。 此类用法可能会导致安全漏洞。

  • 如果部分受信任的代码通过扩展点(代理项)、要序列化的类型或其他方式控制序列化过程,则部分信任的代码可能会导致序列化程序将大量数据输出到序列化流中,这可能会导致拒绝服务(DoS)接收方接收此流。 如果要序列化数据,而这些数据专用于易于遭到 DoS 威胁的目标,则不序列化部分受信任的类型,或以其他方式使部分受信任的代码控制序列化。

  • 如果你允许部分受信任的代码访问 DataContractSerializer 实例,或以其他方式控制数据协定代理项,则代码可能会对序列化/反序列化过程进行大量控制。 例如,它可以注入任意类型、导致信息泄露、篡改生成的对象图或序列化数据,或溢出生成的序列化流。 “在“安全使用 NetDataContractSerializer”部分中描述了一种等效的 NetDataContractSerializer 威胁。”

  • DataContractAttribute 特性如果应用于某个类型(或标记为SerializableAttribute但不是ISerializable类型),那么反序列化程序可以创建这种类型的实例,即使该类型的所有构造函数都是非公共的或受需求保护。

  • 切勿信任反序列化的结果,除非要反序列化的数据受信任,并且你确信所有已知类型都是你信任的类型。 请注意,在运行于部分信任环境时,已知类型不会从应用程序配置文件中加载,而是从计算机配置文件中加载。

  • 如果您使用添加到部分受信任代码的代理项来传递 DataContractSerializer 实例,则代码可以更改该代理项的任何可修改设置。

  • 对于反序列化对象,如果 XML 读取器(或其中的数据)来自部分受信任的代码,则将生成的反序列化对象视为不受信任的数据。

  • ExtensionDataObject 类型没有公共成员的事实并不意味着其中的数据是安全的。 例如,如果从特权数据源反序列化到包含某些数据的对象,并将该对象交给部分信任的代码,那么部分信任的代码可以通过对对象进行序列化来读取ExtensionDataObject中的数据。 当从具有特权的数据源反序列化为之后将传递给部分受信任的代码的对象时,应考虑将 IgnoreExtensionDataObject 设置为 true

  • DataContractSerializerDataContractJsonSerializer 支持完全信任状态下私有、受保护、内部和公共成员的序列化。 但在部分信任的情况下,只可序列化公共成员。 当应用程序尝试序列化非公共成员时,将抛出一个SecurityException

    若要允许在部分信任的情况下序列化内部成员或受保护成员,则请使用 InternalsVisibleToAttribute 程序集属性。 此属性允许程序集声明其内部成员对某些其他程序集可见。 在此情况下,需要序列化其内部成员的程序集可声明其内部成员对 System.Runtime.Serialization.dll 可见。

    此方法的优点是,不需要提升的代码生成路径。

    同时,有两个主要缺点。

    第一个缺点是 InternalsVisibleToAttribute 属性的"选择参与"特性适用于整个程序集。 也就是说,不能指定只有特定类可以对其内部成员进行序列化。 当然,你仍然可以选择不序列化特定内部成员,只需不向该成员添加 DataMemberAttribute 属性即可。 同样,开发人员还可以选择将成员设定为内部成员,而不是私有成员或受保护成员,不过会略有可见性方面的问题。

    第二个缺点是它仍然不支持私有或受保护的成员。

    若要演示如何在部分信任的情况下使用 InternalsVisibleToAttribute 属性,请考虑以下程序:

        public class Program
        {
            public static void Main(string[] args)
            {
                try
                {
    //              PermissionsHelper.InternetZone corresponds to the PermissionSet for partial trust.
    //              PermissionsHelper.InternetZone.PermitOnly();
                    MemoryStream memoryStream = new MemoryStream();
                    new DataContractSerializer(typeof(DataNode)).
                        WriteObject(memoryStream, new DataNode());
                }
                finally
                {
                    CodeAccessPermission.RevertPermitOnly();
                }
            }
    
            [DataContract]
            public class DataNode
            {
                [DataMember]
                internal string Value = "Default";
            }
        }
    

    在上面的示例中, PermissionsHelper.InternetZone 对应于 PermissionSet 部分信任。 现在,如果没有 InternalsVisibleToAttribute 属性,则应用程序将失败,并引发一个 SecurityException,它指示无法以部分信任的方式序列化非公共成员。

    但是,如果将以下行添加到源文件,程序将成功运行。

    [assembly:System.Runtime.CompilerServices.InternalsVisibleTo("System.Runtime.Serialization, PublicKey = 00000000000000000400000000000000")]
    

其他状态管理问题

关于对象状态管理的一些其他问题值得一提:

  • 在使用流媒体传输的流式编程模型时,消息会在到达时被处理。 消息的发送方可能会在传输过程中中止发送操作,如果预期接收更多内容,则代码将处于不可预知的状态。 通常,不要等待流完成,并且不要在基于流的操作中执行在流传输中止时无法回滚的任何工作。 这也适用于消息在流式传输正文后可能出现格式错误的情况(例如,可能缺少 SOAP 信封的结束标签或包含第二个消息正文)。

  • IExtensibleDataObject使用此功能可能会导致发出敏感数据。 如果您将来自不受信任源的数据放入具有 IExtensibleObjectData 的数据协定中,之后在对消息进行签名的安全通道上重新发送这些数据,则可能表明您对这些数据一无所知。 此外,如果同时考虑已知和未知的数据片段,则发送的总体状态可能无效。 通过选择性地将扩展数据属性 null 设置为或选择性禁用 IExtensibleObjectData 该功能来避免这种情况。

架构导入

通常,导入架构以生成类型的过程仅在设计时发生,例如,在 Web 服务上使用 ServiceModel 元数据实用工具工具(Svcutil.exe) 生成客户端类时。 但是,在更高级的方案中,可以在运行时处理架构。 请注意,这样做会使你面临拒绝服务风险。 某些架构可能需要很长时间才能导入。 如果架构可能来自不受信任的源,则不要在此类方案中使用 XmlSerializer 架构导入组件。

特定于 ASP.NET AJAX 集成的威胁

当用户实现 WebScriptEnablingBehaviorWebHttpBehavior时,WCF 会公开一个可以接受 XML 和 JSON 消息的终结点。 但是,XML 读者和 JSON 读者只使用一套读者配额。 某些配额设置可能适用于一个读取器,但对于另一个读取器来说太大。

实现 WebScriptEnablingBehavior时,用户可以选择在终结点公开 JavaScript 代理。 必须考虑以下安全问题:

  • 通过查看 JavaScript 代理,可以获取有关服务的信息(操作名称、参数名称等)。

  • 使用 JavaScript 终结点时,敏感信息和专用信息可能会保留在客户端 Web 浏览器缓存中。

有关组件的注释

WCF 是一个灵活且可自定义的系统。 本主题的大部分内容都侧重于最常见的 WCF 使用方案。 但是,可以通过多种不同的方式编写 WCF 提供的组件。 请务必了解使用每个组件的安全影响。 特别是:

  • 当必须使用 XML 读取器时,请使用XmlDictionaryReader类提供的读取器,而不要使用任何其他读取器。 安全读取器是使用CreateTextReaderCreateBinaryReaderCreateMtomReader方法创建的。 请勿使用该方法 Create 。 始终为阅读器配置安全配额。 仅当与 WCF 中的安全 XML 读取器一起使用时,WCF 中的序列化引擎才安全。

  • 使用 DataContractSerializer 反序列化可能不受信任的数据时,请始终设置该 MaxItemsInObjectGraph 属性。

  • 如果maxSizeOfHeaders没有提供足够的保护,在创建消息时请设置MaxReceivedMessageSize参数。

  • 创建编码器时,请始终配置相关的配额,例如 MaxSessionSizeMaxBufferSize

  • 使用 XPath 消息筛选器时,设置 NodeQuota 限制筛选器访问的 XML 节点量。 不要使用可能需要很长时间才能计算的 XPath 表达式,而无需访问多个节点。

  • 一般情况下,使用接受配额的任何组件时,请了解其安全含义并将其设置为安全值。

另请参阅