Skip to content

Commit c08825a

Browse files
Change RestrictiveXamlXmlReader to use an allow list
- Removes the old deny list logic used by RestrictiveXamlXmlReader - Provides an extensible (via registry) allow list mechanism Ported from release/7.0 branch. Fixes CVE-2022-41089.
1 parent 6098735 commit c08825a

File tree

1 file changed

+109
-140
lines changed

1 file changed

+109
-140
lines changed

src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Markup/RestrictiveXamlXmlReader.cs

Lines changed: 109 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,69 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
//
6-
// Description: This class provides a XamlXmlReader implementation that skips over some known dangerous
7-
// types when calling into the Read method, this is meant to prevent WpfXamlLoader from instantiating.
5+
//
6+
// Description: This class provides a XamlXmlReader implementation that implements an allow-list of legal
7+
// types when calling into the Read method, meant to prevent instantiation of unexpected types.
88
//
99

10-
using System.Collections.Concurrent;
10+
using Microsoft.Win32;
1111
using System.Collections.Generic;
12-
using System.Reflection;
1312
using System.Xaml;
1413
using System.Xml;
15-
using System.Security;
1614

1715
namespace System.Windows.Markup
1816
{
1917
/// <summary>
20-
/// Provides a XamlXmlReader implementation that that skips over some known dangerous types.
18+
/// Provides a XamlXmlReader implementation that that implements an allow-list of legal types.
2119
/// </summary>
2220
internal class RestrictiveXamlXmlReader : System.Xaml.XamlXmlReader
2321
{
24-
/// <summary>
25-
/// The RestrictedTypes in the _restrictedTypes list do not initially contain a Type reference, we use a Type reference to determine whether an incoming Type can be assigned to that Type
26-
/// in order to restrict it or allow it. We cannot get a Type reference without loading the assembly, so we need to first go through the loaded assemblies, and assign the Types
27-
/// that are loaded.
28-
/// </summary>
29-
static RestrictiveXamlXmlReader()
30-
{
31-
_unloadedTypes = new ConcurrentDictionary<string, List<RestrictedType>>();
22+
private const string AllowedTypesForRestrictiveXamlContexts = @"SOFTWARE\Microsoft\.NETFramework\Windows Presentation Foundation\XPSAllowedTypes";
23+
private static readonly HashSet<string> AllXamlNamespaces = new HashSet<string>(XamlLanguage.XamlNamespaces);
24+
private static readonly Type DependencyObjectType = typeof(System.Windows.DependencyObject);
25+
private static readonly HashSet<string> SafeTypesFromRegistry = ReadAllowedTypesForRestrictedXamlContexts();
3226

33-
// Go through the list of restricted types and add each type under the matching assembly entry in the dictionary.
34-
foreach (RestrictedType type in _restrictedTypes)
27+
private static HashSet<string> ReadAllowedTypesForRestrictedXamlContexts()
28+
{
29+
HashSet<string> allowedTypesFromRegistry = new HashSet<string>();
30+
try
3531
{
36-
if (!String.IsNullOrEmpty(type.AssemblyName))
32+
// n.b. Registry64 uses the 32-bit registry in 32-bit operating systems.
33+
// The registry key should have this format and is consistent across netfx & netcore:
34+
//
35+
// [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\Windows Presentation Foundation\XPSAllowedTypes]
36+
// "SomeValue1"="Contoso.Controls.MyControl"
37+
// "SomeValue2"="Fabrikam.Controls.MyOtherControl"
38+
// ...
39+
//
40+
// The value names aren't important. The value data should match Type.FullName (including namespace but not assembly).
41+
// If any value data is exactly "*", this serves as a global opt-out and allows everything through the system.
42+
using (RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64))
3743
{
38-
if(!_unloadedTypes.ContainsKey(type.AssemblyName))
44+
if (hklm != null)
3945
{
40-
_unloadedTypes[type.AssemblyName] = new List<RestrictedType>();
41-
}
42-
43-
_unloadedTypes[type.AssemblyName].Add(type);
44-
}
45-
else
46-
{
47-
// If the RestrictedType entry does not provide an assembly name load it from PresentationFramework,
48-
// this is the current assembly so it's already loaded, just get the Type and assign it to the RestrictedType entry
49-
System.Type typeReference = System.Type.GetType(type.TypeName, false);
50-
51-
if (typeReference != null)
52-
{
53-
type.TypeReference = typeReference;
46+
using (RegistryKey xpsDangerKey = hklm.OpenSubKey(AllowedTypesForRestrictiveXamlContexts, false))
47+
{
48+
if (xpsDangerKey != null)
49+
{
50+
foreach (string typeName in xpsDangerKey.GetValueNames())
51+
{
52+
object value = xpsDangerKey.GetValue(typeName);
53+
if (value != null)
54+
{
55+
allowedTypesFromRegistry.Add(value.ToString());
56+
}
57+
}
58+
}
59+
}
5460
}
5561
}
5662
}
63+
catch
64+
{
65+
// do nothing
66+
}
67+
return allowedTypesFromRegistry;
5768
}
5869

5970
/// <summary>
@@ -85,15 +96,15 @@ internal RestrictiveXamlXmlReader(XmlReader xmlReader, XamlSchemaContext schemaC
8596
/// </returns>
8697
public override bool Read()
8798
{
88-
bool result = false;
99+
bool result;
89100
int skippingDepth = 0;
90101

91102
while (result = base.Read())
92103
{
93104
if (skippingDepth <= 0)
94105
{
95-
if (NodeType == System.Xaml.XamlNodeType.StartObject &&
96-
IsRestrictedType(Type.UnderlyingType))
106+
if ((NodeType == System.Xaml.XamlNodeType.StartObject && !IsAllowedType(Type.UnderlyingType)) ||
107+
(NodeType == System.Xaml.XamlNodeType.StartMember && Member is XamlDirective directive && !IsAllowedDirective(directive)))
97108
{
98109
skippingDepth = 1;
99110
}
@@ -104,14 +115,18 @@ public override bool Read()
104115
}
105116
else
106117
{
107-
if (NodeType == System.Xaml.XamlNodeType.StartObject ||
108-
NodeType == System.Xaml.XamlNodeType.GetObject)
109-
{
110-
skippingDepth += 1;
111-
}
112-
else if (NodeType == System.Xaml.XamlNodeType.EndObject)
118+
switch (NodeType)
113119
{
114-
skippingDepth -= 1;
120+
case System.Xaml.XamlNodeType.StartObject:
121+
case System.Xaml.XamlNodeType.StartMember:
122+
case System.Xaml.XamlNodeType.GetObject:
123+
skippingDepth += 1;
124+
break;
125+
126+
case System.Xaml.XamlNodeType.EndObject:
127+
case System.Xaml.XamlNodeType.EndMember:
128+
skippingDepth -= 1;
129+
break;
115130
}
116131
}
117132
}
@@ -120,128 +135,82 @@ public override bool Read()
120135
}
121136

122137
/// <summary>
123-
/// Determines whether an incoming type is either a restricted type or inheriting from one.
138+
/// Determines whether an incoming directive is allowed.
124139
/// </summary>
125-
private bool IsRestrictedType(Type type)
140+
private bool IsAllowedDirective(XamlDirective directive)
126141
{
127-
if (type != null)
142+
// If the global opt-out switch is enabled, all directives are allowed.
143+
if (SafeTypesFromRegistry.Contains("*"))
128144
{
129-
// If an incoming type is already in the set we can just return false, we've verified this type is safe.
130-
if (_safeTypesSet.Contains(type))
131-
{
132-
return false;
133-
}
134-
135-
// Ensure that the restricted type list has the latest information for the assemblies currently loaded.
136-
EnsureLatestAssemblyLoadInformation();
145+
return true;
146+
}
137147

138-
// Iterate through our _restrictedTypes list, if an entry has a TypeReference then the type is loaded and we can check it.
139-
foreach (RestrictedType restrictedType in _restrictedTypes)
148+
// If this isn't a XAML directive, allow it through.
149+
// This allows XML directives and other non-XAML directives through.
150+
// This largely follows the logic at XamlMember.Equals, but we trigger for *any*
151+
// overlapping namespace rather than requiring the namespace sets to match exactly.
152+
bool isXamlDirective = false;
153+
foreach (string xmlns in directive.GetXamlNamespaces())
154+
{
155+
if (AllXamlNamespaces.Contains(xmlns))
140156
{
141-
if (restrictedType.TypeReference?.IsAssignableFrom(type) == true)
142-
{
143-
return true;
144-
}
157+
isXamlDirective = true;
158+
break;
145159
}
160+
}
146161

147-
// We've detected this type isn't nor inherits from a restricted type, add it to the safe types set.
148-
_safeTypesSet.Add(type);
162+
if (!isXamlDirective)
163+
{
164+
return true;
165+
}
166+
167+
// The following is an exhaustive list of all allowed XAML directives.
168+
if (directive.Name == XamlLanguage.Items.Name ||
169+
directive.Name == XamlLanguage.Key.Name ||
170+
directive.Name == XamlLanguage.Name.Name)
171+
{
172+
return true;
149173
}
150174

175+
// This is a XAML directive but isn't in the allow-list; forbid it.
151176
return false;
152177
}
153178

154179
/// <summary>
155-
/// Iterates through the currently loaded assemblies and gets the Types for the assemblies we've marked as unloaded.
156-
/// If our thread static assembly count is still the same we can skip this, we know no new assemblies have been loaded.
180+
/// Determines whether an incoming type is present in the allow list.
157181
/// </summary>
158-
private static void EnsureLatestAssemblyLoadInformation()
182+
private bool IsAllowedType(Type type)
159183
{
160-
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
161-
162-
if (assemblies.Length != _loadedAssembliesCount)
184+
// If the global opt-out switch is enabled, or if this type has been explicitly
185+
// allow-listed (or is null, meaning this is a proxy which will be checked elsewhere),
186+
// then it can come through.
187+
if (type is null || SafeTypesFromRegistry.Contains("*") || _safeTypesSet.Contains(type) || SafeTypesFromRegistry.Contains(type.FullName))
163188
{
164-
foreach (Assembly assembly in assemblies)
165-
{
166-
RegisterAssembly(assembly);
167-
}
168-
169-
_loadedAssembliesCount = assemblies.Length;
189+
return true;
170190
}
171-
}
172191

192+
// We also have an implicit allow list which consists of:
193+
// - primitives (int, etc.); and
194+
// - any DependencyObject-derived type which exists in the System.Windows.* namespace.
173195

174-
/// <summary>
175-
/// Get the Types from a newly loaded assembly and assign them in the RestrictedType list.
176-
/// </summary>
177-
private static void RegisterAssembly(Assembly assembly)
178-
{
179-
if (assembly != null)
180-
{
181-
string fullName = assembly.FullName;
196+
bool isValidNamespace = type.Namespace != null && type.Namespace.StartsWith("System.Windows.", StringComparison.Ordinal);
197+
bool isValidSubClass = type.IsSubclassOf(DependencyObjectType);
198+
bool isValidPrimitive = type.IsPrimitive;
182199

183-
List<RestrictedType> types = null;
184-
if (_unloadedTypes.TryGetValue(fullName, out types))
185-
{
186-
if (types != null)
187-
{
188-
foreach (RestrictedType restrictedType in types)
189-
{
190-
Type typeInfo = assembly.GetType(restrictedType.TypeName, false);
191-
restrictedType.TypeReference = typeInfo;
192-
}
193-
}
194-
195-
_unloadedTypes.TryRemove(fullName, out types);
196-
}
200+
if (isValidPrimitive || (isValidNamespace && isValidSubClass))
201+
{
202+
// Add it to the explicit allow list to make future lookups on this instance faster.
203+
_safeTypesSet.Add(type);
204+
return true;
197205
}
206+
207+
// Otherwise, it didn't exist on any of our allow lists.
208+
return false;
198209
}
199210

200211
/// <summary>
201-
/// Known dangerous types exploitable through XAML load.
202-
/// </summary>
203-
static List<RestrictedType> _restrictedTypes = new List<RestrictedType>() {
204-
new RestrictedType("System.Windows.Data.ObjectDataProvider",""),
205-
new RestrictedType("System.Windows.ResourceDictionary",""),
206-
new RestrictedType("System.Configuration.Install.AssemblyInstaller","System.Configuration.Install, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"),
207-
new RestrictedType("System.Activities.Presentation.WorkflowDesigner","System.Activities.Presentation, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = 31bf3856ad364e35"),
208-
new RestrictedType("System.Windows.Forms.BindingSource","System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
209-
};
210-
/// <summary>
211-
/// Dictionary to keep track of known types that have not yet been loaded, the key is the assembly name.
212-
/// Once an assembly is loaded we take the known types in that assembly, set the Type property in the RestrictedType entry, then remove the assembly entry.
213-
/// </summary>
214-
static ConcurrentDictionary<string, List<RestrictedType>> _unloadedTypes = null;
215-
216-
/// <summary>
217-
/// Per instance set of found restricted types, this is initialized to the known types that have been loaded,
218-
/// if a type is found that is not already in the set and is assignable to a restricted type it is added.
212+
/// Per instance set of allow-listed types, may grow at runtime to encompass implicit allow list.
219213
/// </summary>
220214
HashSet<Type> _safeTypesSet = new HashSet<Type>();
221-
222-
/// <summary>
223-
/// Keeps track of the assembly count, if the assembly count is the same we avoid having to go through the loaded assemblies.
224-
/// Once we get a Type in IsRestrictedType we are guaranteed to have already loaded it's assembly and base type assemblies,any other
225-
/// assembly loads happening in other threads during our processing of IsRestrictedType do not affect our ability to check the Type properly.
226-
/// </summary>
227-
[ThreadStatic] private static int _loadedAssembliesCount;
228-
229-
/// <summary>
230-
/// Helper class to store type names.
231-
/// </summary>
232-
private class RestrictedType
233-
{
234-
public RestrictedType(string typeName, string assemblyName)
235-
{
236-
TypeName = typeName;
237-
AssemblyName = assemblyName;
238-
}
239-
240-
public string TypeName { get; set; }
241-
public string AssemblyName { get; set; }
242-
public Type TypeReference { get; set; }
243-
}
244215
}
245216
}
246-
247-

0 commit comments

Comments
 (0)