
.NET 安全设计文档解析System.StringComparer 的安全边界与对抗性输入防御实践【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读本文以 .NET 官方安全设计文档 System.StringComparer.md 为骨架结合 .NET 运行时仓库中StringComparer的实际源码与测试系统讲解StringComparer在对抗性输入adversarial input下的安全承诺与边界哪些 API 对恶意输入安全、哪些不安全、为什么Dictionarystring, .../HashSetstring内置集合有特殊保护、以及跨运行时/跨操作系统的全球化数据差异NLS 与 ICU会如何被攻击者利用。读完本文你将掌握在 Web 服务、认证授权、数据持久化等场景中正确选择与使用字符串比较器的实战方法。概述StringComparer 是什么安全承诺在哪里System.StringComparer用于三件事确定两个字符串的排序顺序Compare、确定两个字符串是否相等Equals、以及为字符串键计算哈希码以用于分桶键控集合GetHashCode。每个比较器实例都封装了特定的相等性/排序逻辑可以是基于字符串中每个字符数值的序数比较ordinal comparison也可以是依据人类语言规则的语言比较linguistic comparison。变体可能包括切换大小写敏感性、忽略变音符diacritics、将连字视为其底层字素序列的等价物、忽略片假名的字符宽度等。核心安全结论文档 SummaryStringComparer.Ordinal与StringComparer.OrdinalIgnoreCase这两个单例在提供对抗性输入时被保证安全可放心传入Dictionarystring, ...和HashSetstring的构造函数这些集合实例能够抵御恶意输入。但在这些特定集合类型之外直接使用StringComparer实例带有特殊的安全考量尤其是当依赖比较器的数据已被持久化时。文档覆盖 .NET Framework 4.x 与 .NET 6在受支持的操作系统上并明确排除运行时之外的自定义StringComparer派生类型。面向的读者StringComparer 的维护者需要理解并保持该类型的行为保证StringComparer 的使用者依赖这些保证需要在安全敏感场景中做出正确选择。假设与依赖NLS 与 ICUStringComparer内部依赖System.Globalization.CultureInfo与System.Globalization.CompareInfo。而CompareInfo又依赖两种底层全球化实现实现使用场景NLSNational Language Support.NET Framework 4.x 使用.NET 6 应用在 Windows 上可选用ICUInternational Components for Unicode.NET 6 在所有可用操作系统上默认使用两者都不使用的环境不在本文档覆盖范围内。从源码结构看语言感知比较器CultureAwareComparer的Compare、Equals、GetHashCode全部转发到内部_compareInfoCompareInfo实例见 src/libraries/System.Private.CoreLib/src/System/StringComparer.cs因此 NLS/ICU 的行为差异会直接穿透到StringComparer的语言比较结果上。API 使用与安全考量下文记号约定对任意长度的 UTF-16 / UCS-2 输入字符串 $s$$\mathit{len}(s)$ 表示其char数量双参方法用 $s_1, s_2$ 表示两个输入序数比较器涵盖Ordinal与OrdinalIgnoreCase语言比较器指所有非序数比较器包括InvariantCulture、InvariantCultureIgnoreCase等文化无关的比较器。Compare 与 Equals序数比较器序数比较方法保证能抵抗对抗性输入。恶意输入不会在相等性方法内部造成拒绝服务、缓冲区溢出或其他异常行为即便内部逻辑失败例如输入字符串过长也会优雅地抛出异常。复杂度保证文档给出的最坏情况上界比较的最坏情况运行时间为 $O\left(\min \left(\mathit{len}(s_1), \mathit{len}(s_2)\right)\right)$若两个输入字符串从开头起在第 $i$ 个字符索引处首次不同则比较的最坏情况运行时间为 $O(i)$。相等性检查可能提供进一步优化例如两个输入长度不同时提前退出但这些优化不是保证对 UTF-8 这类可变长字符集甚至无法保证调用方不得依赖此类优化。这一点从OrdinalComparer.Equals的实现可见一斑OrdinalIgnoreCase.Equals确实先做x.Length ! y.Length的长度检查以提前返回见 src/libraries/System.Private.CoreLib/src/System/StringComparer.cs。大小写不敏感比较在 NLS 与 ICU 之间不一致对大小写不敏感比较Compare与Equals的返回值不保证在 NLS 与 ICU 之间一致。例如StringComparer.OrdinalIgnoreCase.Equals(\U00010CD4, \U00010C94) // 在较新版本 ICU 上返回 true在较新版本 NLS 上返回 false此外操作系统与 .NET 运行时更新都可能升级 NLS/ICU 版本。一旦发生先前在不区分大小写比较器下判为不相等的字符串可能开始判为相等。例如码点U10595与U105BC直到 Unicode 14.0 才被分配ICU 模式下.NET 6 运行时内嵌 Unicode 13.0 大小写表判定StringComparer.OrdinalIgnoreCase.Equals(\U00010595, \U000105BC)为false而 .NET 8 运行时内嵌 Unicode 15.0 大小写表判定同一语句为true。Tip依赖不区分大小写的相等性比较来做安全决策的应用如用户名比较应限制允许的字符集合避免 ICU/NLS 升级引发冲突。.NET 还提供CompareInfo.IsSortable方法可用于判断输入数据是否仅由运行时所用 NLS 版本已知的已分配字符组成。但IsSortable在 ICU 上语义不同不应用于判断底层 ICU 库是否认为输入仅由已分配字符组成。混淆攻击confusion attacks跨语言不一致采用多种语言编写组件、或分布在异构操作系统环境中的服务必须警惕混淆攻击。.NET 的大小写不敏感比较先各自转成大写再比较在 NLS 中通过 Win32CompareStringOrdinalAPI 完成在 ICU 中 .NET 查询底层 ICU 库的大小写表若运行在 Invariant globalization 模式.NET 查询自己本地保存的 ICU 大小写表以便在 ICU 上模仿旧版CompareStringOrdinal行为。这意味着 .NET 对两个字符串是否大小写相等的判断可能与其它语言不一致即使输入完全相同。例如// .NET Framework 与 .NET 6 - 输出 FALSE Console.WriteLine(StringComparer.OrdinalIgnoreCase.Equals(Administrator, Adminiſtrator));// go - 输出 TRUE fmt.Println(strings.EqualFold(Administrator, Adminiſtrator))// Java - 输出 TRUE System.out.println(Administrator.equalsIgnoreCase(Adminiſtrator));Adminiſtrator含U017F LATIN SMALL LETTER LONG S。如果攻击者能让同一服务中两个不同子系统对两个字符串是否大小写相等得出相反结论就可能颠覆服务业务逻辑甚至提升自身权限。Compare 与 Equals语言比较器语言比较方法不保证能抵抗对抗性输入ICU 与 NLS 都没有发布面对对抗性输入时比较例程行为如算法复杂度攻击下的保证实践中观察 ICU/NLS 的语言比较器以 $O\left(\mathit{len}(s_1) \mathit{len}(s_2)\right)$ 运行但这不是公开保证——若底层算法存在回溯复杂度可能膨胀到 $O\left(\mathit{len}(s_1) \cdot \mathit{len}(s_2)\right)$ 等不良上限若两输入字符串从开头起第 $i$ 处首次不同也不保证语言比较最坏情况运行时间为 $O(i)$比较与相等性结果取决于 ICU/NLS 版本。OS 提升全球化数据质量时相等性判定规则可能变化。切换 ICU/NLS、升级 OS 或 ICU 版本时应用可能观察到比较结果变化——即使两个输入字符串的CompareInfo.IsSortable均返回true。Caution处理表单字段、用户名等安全敏感标识符时不应使用语言比较器序数比较器更合适。哈希码计算GetHashCode序数比较器序数比较器的哈希码计算例程保证抵抗对抗性输入保证讨论同比较章节。但StringComparer明确声明由对抗性输入生成的哈希码不适合调用方直接消费——计算例程本身抗恶意输入但得到的哈希码可能有碰撞或其它模式不适合直接使用。部分 .NET 运行时在哈希码计算中把Marvin32作为实现细节使用其种子在应用启动时随机选择。虽然 Marvin32 看起来是健壮的算法但不应依赖它来抵抗哈希洪水种子无法轮换增加了侧信道或哈希码输出泄露给对手后重建种子、制造碰撞的风险。NoteDictionarystring, ...与HashSetstring等内置集合会对调用方传入StringComparer.Ordinal/StringComparer.OrdinalIgnoreCase作为集合构造函数的comparer参数这一情况做特判此时集合实例使用一种对哈希洪水攻击有韧性的特殊哈希码计算实现。复杂度保证哈希码计算为 $O\left(\mathit{len}(s)\right)$。实现因运行时而异甚至同一应用在同一机器上的两次调用之间也可能不同。哈希码值在生成它的应用之外毫无意义不应传出应用或持久化。不同的StringComparer实例对同一输入可能返回不同哈希码例如StringComparer.Ordinal.GetHashCode(s)与StringComparer.OrdinalIgnoreCase.GetHashCode(s)即使输入相同字符串 $s$ 也可能不同。语言比较器语言比较器的哈希码计算方法不保证抵抗对抗性输入。GetHashCode依赖 ICU/NLS 的排序键sort key概念而两者均未发布排序键计算例程面对对抗性输入时的保证实践中观察其以 $O\left(\mathit{len}(s)\right)$ 运行但同样非公开保证回溯下可能膨胀到 $O\left(\mathit{len}(s)^2\right)$。与序数比较器相同语言比较器产生的哈希码不保证跨应用调用稳定不应传输或持久化对抗性输入产生的哈希码不保证适合直接消费。Note内置集合类型同样会特判内置的语言StringComparer实例此时集合使用对哈希洪水攻击有韧性的特殊哈希码计算实现。但自定义哈希码计算例程仍会受到底层 ICU/NLS 排序键生产 API 中任何灾难性回溯或其他漏洞的影响。源码中的特判机制IsWellKnownOrdinalComparer文档反复强调的内置集合特判在源码中有明确落点。StringComparer提供两个公开工厂式判定 API源码见 src/libraries/System.Private.CoreLib/src/System/StringComparer.csIsWellKnownOrdinalComparer(IEqualityComparerstring? comparer, out bool ignoreCase)判断给定比较器是否为良知的序数比较器行为等价于传给Dictionary/HashSet构造函数时的Ordinal/OrdinalIgnoreCase。EqualityComparerstring.Default也返回true因为它与Ordinal行为一致。IsWellKnownCultureAwareComparer(...)判断是否为绑定到特定CompareInfoCompareOptions的良知的文化感知比较器。OrdinalComparer通过IsWellKnownOrdinalComparerCore返回_ignoreCase标记见 src/libraries/System.Private.CoreLib/src/System/StringComparer.cs。Dictionary与HashSet的构造函数则会通过IInternalStringEqualityComparer.GetUnderlyingEqualityComparer解开包装、识别出这些内置比较器进而切换到带随机种子、抗哈希洪水的专用哈希路径相关调用见 src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs 与 src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs。这些行为有对应的单元测试覆盖例如 StringComparerTests.cs 中的IsWellKnownOrdinalComparer_TestCases与IsWellKnownCultureAwareComparer_TestCases。从源码结构看这一机制的设计意图正是文档所述把抗哈希洪水的保护逻辑做成Dictionary等内置集合的实现细节而不是让StringComparer单例本身承担抗碰撞责任。与其他内置比较器的关系EqualityComparer .Default这是Dictionarystring, ...、HashSetstring在未显式提供比较器时使用的默认比较器。它返回的比较器功能上等价于StringComparer.OrdinalEquals与GetHashCode行为均如前述序数章节所述。Comparer .Default这是SortedDictionarystring, ...、SortedSetstring未显式提供比较器时的默认比较器也是Array.Sortstring(string[])等工具方法的默认比较器。它返回的比较器基本等价于StringComparer.CurrentCultureCompare行为如语言章节所述。两者的关键区别在于当前文化对象何时被捕获查询StringComparer.CurrentCulture属性时立即捕获CultureInfo.CurrentCulture的线程局部值返回的StringComparer实例被锁定到该文化对象即使活动线程文化随后改变该实例仍使用捕获的文化对象。Comparerstring.Default属性 getter 则返回一个不锁定到任何特定文化的特殊比较器对象每次调用Compare时查询线程的当前文化对象。using System; using System.Collections.Generic; using System.Globalization; CultureInfo.CurrentCulture CultureInfo.GetCultureInfo(en-US); var comparer1 StringComparer.CurrentCulture; CultureInfo.CurrentCulture CultureInfo.GetCultureInfo(fr-FR); var result1 comparer1.Compare(string1, string2); // 使用 en-US 规则 CultureInfo.CurrentCulture CultureInfo.GetCultureInfo(en-US); var comparer2 ComparerString.Default; CultureInfo.CurrentCulture CultureInfo.GetCultureInfo(fr-FR); var result2 comparer2.Compare(string1, string2); // 使用 fr-FR 规则在源码中StringComparer.CurrentCulture每次访问都执行new CultureAwareComparer(CultureInfo.CurrentCulture, CompareOptions.None)见 src/libraries/System.Private.CoreLib/src/System/StringComparer.cs正对应查询时立即捕获线程当前文化的语义。集合类型的安全考量由于集合类型在未提供显式比较器时可能使用EqualityComparerstring.Default或Comparerstring.Default而二者比较逻辑不同调用方应仔细阅读所使用集合类型的文档确认默认行为适用于自己的场景。当键可能由攻击者提供时这一点尤其重要攻击者可能精心构造键强制字典中的条目被覆盖而不是新增用对抗性值覆盖可信条目从而颠覆应用逻辑。下面的例子展示 Web 应用中两个组件——一个用Dictionarystring, ...另一个用SortedDictionarystring, ...——解析同一查询字符串却对载荷中的值得出不同结论using System; using System.Collections.Generic; using Microsoft.AspNetCore.WebUtilities; const string QueryString ?fruitbananafruit%e2%80%8dorange; // [E2 80 8D] U200D ZERO-WIDTH JOINER // 用默认构造函数的 Dictionarystring, ... 解析查询字符串 var normalDict new Dictionarystring, string(); foreach (var kvp in new QueryStringEnumerable(QueryString)) { normalDict[kvp.DecodeName().ToString()] kvp.DecodeValue().ToString(); } Console.WriteLine($normalDict[fruit] {normalDict[fruit]}); // banana // 用默认构造函数的 SortedDictionarystring, ... 解析查询字符串 var sortedDict new SortedDictionarystring, string(); foreach (var kvp in new QueryStringEnumerable(QueryString)) { sortedDict[kvp.DecodeName().ToString()] kvp.DecodeValue().ToString(); } Console.WriteLine($sortedDict[fruit] {sortedDict[fruit]}); // orangeU200D ZERO-WIDTH JOINER的存在使两个集合对键是否相等给出不同判断导致条目互相覆盖的结果不同。这种不一致可被用于混淆攻击。Tip如果不确定集合的默认行为请向集合构造函数显式传入StringComparer对象最好是Ordinal或OrdinalIgnoreCase以消除歧义。另外考虑使用集合类型的Add方法而非索引器赋值。对大多数集合类型Add会在调用方试图添加重复条目时抛出异常。持久化存储与数据传输的安全考量前述章节讨论过两个字符串的相等性或排序比较可能因操作系统或运行时版本而异。这给攻击者提供了滥用机会。设想一个 Web 服务从客户端接收字符串数组用OrdinalIgnoreCase或语言比较器排序后持久化到存储。服务的另一组件从数据库加载该条目并基于写入方已排序的认知执行假设数据已正确排序的操作。由于应用内不同组件可能运行在不同运行时或不同环境中它们对正确排序的定义可能不同。如果操作的是恶意输入攻击者就有机会操纵目标服务内的控制流。下面演示这一风险。假设处理初始请求并把排序后数据写入数据库的应用是 .NET Framework 4.x/* 在 .NET Framework 4.x 中调用 */ using System; using System.Linq; using Microsoft.AspNetCore.WebUtilities; // 从请求读取字符串此处为演示硬编码 string userInput ?keywordstargetkeywordstar%C6%84getkeywordstar%C6%86getkeywordszulu; var keywords QueryHelpers.ParseQuery(userInput); // 来自 Microsoft.AspNetCore.WebUtilities 包 Array.Sort(keywords); // 使用 Comparerstring.Default WriteToDatabase(keywords); // 顺序: [ tar\u0184get, tar\u0186get, target, zulu ]再假设从数据库加载的应用是运行在默认 ICU 模式下的 .NET 8/* 在 .NET 8 中调用 */ using System; // 从数据库读取先前排序的数组此处为演示硬编码 string[] sortedKeywords [tar\u0184get, tar\u0186get, target, zulu]; Console.WriteLine($Array.BinarySearch(..., target) {Array.BinarySearch(sortedKeywords, target)}); // 输出 -1数据持久化时排序依据的是执行数据库写入的应用的规则而非执行数据库读取的应用的规则。如果攻击者知道读取应用在控制流逻辑中使用了Array.BinarySearch并能构造违反二分搜索不变量的载荷就可能胁迫读取应用进入本不该执行的逻辑分支。上例中攻击者能胁迫BinarySearch返回负值表示字符串target不在输入数组中。Tip若数据的来源可能具有对抗性应用在操作数据前应考虑重新验证数据的良好结构与完整性。即使某个可信组件已独立验证过数据也不能假定该可信组件先前的验证与当前组件的验证完全吻合。未来改进与考量通过 API 修改提升哈希洪水抵抗力StringComparer不保证对抗性影响的哈希码的可用性这可能使开发者难以创建自己的安全键控集合类型——因为该保护逻辑目前是Dictionarystring, ...等内置集合的实现细节。为促进健康的第三方安全键控集合生态系统值得研究允许StringComparer使用按实例随机化而非单一全局随机种子。现有工厂方法StringComparer.FromComparison与StringComparer.Create很容易改造成支持这一点。但这无法解决大多数开发者目前使用静态单例属性如Ordinal、OrdinalIgnoreCase的问题——那可能需要重构 .NET 生态中大量代码。该节与System.HashCode安全设计文档 的Future improvements and considerations章节相互呼应后者对潜在选项有更详细的讨论包括按实例随机化的哈希器设计、IHashable接口方案等。哈希洪水风险的整体背景与DictionaryTKey, TValue的防御机制可进一步参考 System.Collections.Generic.Dictionary.md。小结安全使用 StringComparer 的检查清单安全敏感标识符用户名、表单字段、安全令牌使用序数比较器Ordinal/OrdinalIgnoreCase不要用语言比较器键控集合构造向Dictionary/HashSet构造函数显式传入Ordinal或OrdinalIgnoreCase让内置抗哈希洪水的特判路径生效不确定性不要把哈希码传出应用或持久化不要假设哈希码跨进程、跨运行时稳定排序/相等的跨环境不一致写库与读库、不同组件之间若依赖比较顺序需警惕 NLS/ICU 版本差异导致的混淆攻击操作前重新验证数据集合默认行为不确定Dictionary与SortedDictionary等默认比较器差异时显式传比较器并优先用Add而非索引器赋值以阻止重复键覆盖。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考