Skip to content

Commit 5acaa9e

Browse files
khellangJamesNK
andauthored
Improve usage of Type.GetType when activating types in data protection (#54256)
Co-authored-by: James Newton-King <james@newtonking.com>
1 parent 1aa1c5b commit 5acaa9e

File tree

6 files changed

+173
-14
lines changed

6 files changed

+173
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace Microsoft.AspNetCore.DataProtection.Internal;
8+
9+
internal sealed class DefaultTypeNameResolver : ITypeNameResolver
10+
{
11+
public static readonly DefaultTypeNameResolver Instance = new();
12+
13+
private DefaultTypeNameResolver()
14+
{
15+
}
16+
17+
[UnconditionalSuppressMessage("Trimmer", "IL2057", Justification = "Type.GetType is only used to resolve statically known types that are referenced by DataProtection assembly.")]
18+
public bool TryResolveType(string typeName, [NotNullWhen(true)] out Type? type)
19+
{
20+
try
21+
{
22+
// Some exceptions are thrown regardless of the value of throwOnError.
23+
// For example, if the type is found but cannot be loaded,
24+
// a System.TypeLoadException is thrown even if throwOnError is false.
25+
type = Type.GetType(typeName, throwOnError: false);
26+
return type != null;
27+
}
28+
catch
29+
{
30+
type = null;
31+
return false;
32+
}
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace Microsoft.AspNetCore.DataProtection.Internal;
8+
9+
internal interface ITypeNameResolver
10+
{
11+
bool TryResolveType(string typeName, [NotNullWhen(true)] out Type? type);
12+
}

src/DataProtection/DataProtection/src/KeyManagement/XmlKeyManager.cs

+10-7
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public sealed class XmlKeyManager : IKeyManager, IInternalXmlKeyManager
4949
private const string RevokeAllKeysValue = "*";
5050

5151
private readonly IActivator _activator;
52+
private readonly ITypeNameResolver _typeNameResolver;
5253
private readonly AlgorithmConfiguration _authenticatedEncryptorConfiguration;
5354
private readonly IKeyEscrowSink? _keyEscrowSink;
5455
private readonly IInternalXmlKeyManager _internalKeyManager;
@@ -112,6 +113,8 @@ internal XmlKeyManager(
112113
var escrowSinks = keyManagementOptions.Value.KeyEscrowSinks;
113114
_keyEscrowSink = escrowSinks.Count > 0 ? new AggregateKeyEscrowSink(escrowSinks) : null;
114115
_activator = activator;
116+
// Note: ITypeNameResolver is only implemented on the activator in tests. In production, it's always DefaultTypeNameResolver.
117+
_typeNameResolver = activator as ITypeNameResolver ?? DefaultTypeNameResolver.Instance;
115118
TriggerAndResetCacheExpirationToken(suppressLogging: true);
116119
_internalKeyManager = _internalKeyManager ?? this;
117120
_encryptorFactories = keyManagementOptions.Value.AuthenticatedEncryptorFactories;
@@ -463,27 +466,27 @@ IAuthenticatedEncryptorDescriptor IInternalXmlKeyManager.DeserializeDescriptorFr
463466
}
464467
}
465468

466-
[UnconditionalSuppressMessage("Trimmer", "IL2057", Justification = "Type.GetType result is only useful with types that are referenced by DataProtection assembly.")]
467469
private IAuthenticatedEncryptorDescriptorDeserializer CreateDeserializer(string descriptorDeserializerTypeName)
468470
{
469-
var resolvedTypeName = TypeForwardingActivator.TryForwardTypeName(descriptorDeserializerTypeName, out var forwardedTypeName)
471+
// typeNameToMatch will be used for matching against known types but not passed to the activator.
472+
// The activator will do its own forwarding.
473+
var typeNameToMatch = TypeForwardingActivator.TryForwardTypeName(descriptorDeserializerTypeName, out var forwardedTypeName)
470474
? forwardedTypeName
471475
: descriptorDeserializerTypeName;
472-
var type = Type.GetType(resolvedTypeName, throwOnError: false);
473476

474-
if (type == typeof(AuthenticatedEncryptorDescriptorDeserializer))
477+
if (typeof(AuthenticatedEncryptorDescriptorDeserializer).MatchName(typeNameToMatch, _typeNameResolver))
475478
{
476479
return _activator.CreateInstance<AuthenticatedEncryptorDescriptorDeserializer>(descriptorDeserializerTypeName);
477480
}
478-
else if (type == typeof(CngCbcAuthenticatedEncryptorDescriptorDeserializer) && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
481+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && typeof(CngCbcAuthenticatedEncryptorDescriptorDeserializer).MatchName(typeNameToMatch, _typeNameResolver))
479482
{
480483
return _activator.CreateInstance<CngCbcAuthenticatedEncryptorDescriptorDeserializer>(descriptorDeserializerTypeName);
481484
}
482-
else if (type == typeof(CngGcmAuthenticatedEncryptorDescriptorDeserializer) && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
485+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && typeof(CngGcmAuthenticatedEncryptorDescriptorDeserializer).MatchName(typeNameToMatch, _typeNameResolver))
483486
{
484487
return _activator.CreateInstance<CngGcmAuthenticatedEncryptorDescriptorDeserializer>(descriptorDeserializerTypeName);
485488
}
486-
else if (type == typeof(ManagedAuthenticatedEncryptorDescriptorDeserializer))
489+
else if (typeof(ManagedAuthenticatedEncryptorDescriptorDeserializer).MatchName(typeNameToMatch, _typeNameResolver))
487490
{
488491
return _activator.CreateInstance<ManagedAuthenticatedEncryptorDescriptorDeserializer>(descriptorDeserializerTypeName);
489492
}

src/DataProtection/DataProtection/src/TypeExtensions.cs

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.AspNetCore.DataProtection.Internal;
67

78
namespace Microsoft.AspNetCore.DataProtection;
89

@@ -39,4 +40,16 @@ public static Type GetTypeWithTrimFriendlyErrorMessage(string typeName)
3940
throw new InvalidOperationException($"Unable to load type '{typeName}'. If the app is published with trimming then this type may have been trimmed. Ensure the type's assembly is excluded from trimming.", ex);
4041
}
4142
}
43+
44+
public static bool MatchName(this Type matchType, string resolvedTypeName, ITypeNameResolver typeNameResolver)
45+
{
46+
// Before attempting to resolve the name to a type, check if it starts with the full name of the type.
47+
// Use StartsWith to ignore potential assembly version differences.
48+
if (matchType.FullName != null && resolvedTypeName.StartsWith(matchType.FullName, StringComparison.Ordinal))
49+
{
50+
return typeNameResolver.TryResolveType(resolvedTypeName, out var resolvedType) && resolvedType == matchType;
51+
}
52+
53+
return false;
54+
}
4255
}

src/DataProtection/DataProtection/src/XmlEncryption/XmlEncryptionExtensions.cs

+10-7
Original file line numberDiff line numberDiff line change
@@ -67,27 +67,30 @@ public static XElement DecryptElement(this XElement element, IActivator activato
6767
return doc.Root!;
6868
}
6969

70-
[UnconditionalSuppressMessage("Trimmer", "IL2057", Justification = "Type.GetType result is only useful with types that are referenced by DataProtection assembly.")]
7170
private static IXmlDecryptor CreateDecryptor(IActivator activator, string decryptorTypeName)
7271
{
73-
var resolvedTypeName = TypeForwardingActivator.TryForwardTypeName(decryptorTypeName, out var forwardedTypeName)
72+
// typeNameToMatch will be used for matching against known types but not passed to the activator.
73+
// The activator will do its own forwarding.
74+
var typeNameToMatch = TypeForwardingActivator.TryForwardTypeName(decryptorTypeName, out var forwardedTypeName)
7475
? forwardedTypeName
7576
: decryptorTypeName;
76-
var type = Type.GetType(resolvedTypeName, throwOnError: false);
7777

78-
if (type == typeof(DpapiNGXmlDecryptor))
78+
// Note: ITypeNameResolver is only implemented on the activator in tests. In production, it's always DefaultTypeNameResolver.
79+
var typeNameResolver = activator as ITypeNameResolver ?? DefaultTypeNameResolver.Instance;
80+
81+
if (typeof(DpapiNGXmlDecryptor).MatchName(typeNameToMatch, typeNameResolver))
7982
{
8083
return activator.CreateInstance<DpapiNGXmlDecryptor>(decryptorTypeName);
8184
}
82-
else if (type == typeof(DpapiXmlDecryptor))
85+
else if (typeof(DpapiXmlDecryptor).MatchName(typeNameToMatch, typeNameResolver))
8386
{
8487
return activator.CreateInstance<DpapiXmlDecryptor>(decryptorTypeName);
8588
}
86-
else if (type == typeof(EncryptedXmlDecryptor))
89+
else if (typeof(EncryptedXmlDecryptor).MatchName(typeNameToMatch, typeNameResolver))
8790
{
8891
return activator.CreateInstance<EncryptedXmlDecryptor>(decryptorTypeName);
8992
}
90-
else if (type == typeof(NullXmlDecryptor))
93+
else if (typeof(NullXmlDecryptor).MatchName(typeNameToMatch, typeNameResolver))
9194
{
9295
return activator.CreateInstance<NullXmlDecryptor>(decryptorTypeName);
9396
}

src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/XmlEncryption/XmlEncryptionExtensionsTests.cs

+94
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,100 @@ public void DecryptElement_RootNodeRequiresDecryption_Success()
4949
XmlAssert.Equal("<newNode />", retVal);
5050
}
5151

52+
[Fact]
53+
public void DecryptElement_CustomType_TypeNameResolverNotCalled()
54+
{
55+
// Arrange
56+
var decryptorTypeName = typeof(MyXmlDecryptor).AssemblyQualifiedName;
57+
58+
var original = XElement.Parse(@$"
59+
<x:encryptedSecret decryptorType='{decryptorTypeName}' xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
60+
<node />
61+
</x:encryptedSecret>");
62+
63+
var mockActivator = new Mock<IActivator>();
64+
mockActivator.ReturnDecryptedElementGivenDecryptorTypeNameAndInput(decryptorTypeName, "<node />", "<newNode />");
65+
var mockTypeNameResolver = mockActivator.As<ITypeNameResolver>();
66+
67+
var serviceCollection = new ServiceCollection();
68+
serviceCollection.AddSingleton<IActivator>(mockActivator.Object);
69+
var services = serviceCollection.BuildServiceProvider();
70+
var activator = services.GetActivator();
71+
72+
// Act
73+
var retVal = original.DecryptElement(activator);
74+
75+
// Assert
76+
XmlAssert.Equal("<newNode />", retVal);
77+
Type resolvedType;
78+
mockTypeNameResolver.Verify(o => o.TryResolveType(It.IsAny<string>(), out resolvedType), Times.Never());
79+
}
80+
81+
[Fact]
82+
public void DecryptElement_KnownType_TypeNameResolverCalled()
83+
{
84+
// Arrange
85+
var decryptorTypeName = typeof(NullXmlDecryptor).AssemblyQualifiedName;
86+
TypeForwardingActivator.TryForwardTypeName(decryptorTypeName, out var forwardedTypeName);
87+
88+
var original = XElement.Parse(@$"
89+
<x:encryptedSecret decryptorType='{decryptorTypeName}' xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
90+
<node>
91+
<value />
92+
</node>
93+
</x:encryptedSecret>");
94+
95+
var mockActivator = new Mock<IActivator>();
96+
mockActivator.Setup(o => o.CreateInstance(typeof(NullXmlDecryptor), decryptorTypeName)).Returns(new NullXmlDecryptor());
97+
var mockTypeNameResolver = mockActivator.As<ITypeNameResolver>();
98+
var resolvedType = typeof(NullXmlDecryptor);
99+
mockTypeNameResolver.Setup(mockTypeNameResolver => mockTypeNameResolver.TryResolveType(forwardedTypeName, out resolvedType)).Returns(true);
100+
101+
var serviceCollection = new ServiceCollection();
102+
serviceCollection.AddSingleton<IActivator>(mockActivator.Object);
103+
var services = serviceCollection.BuildServiceProvider();
104+
var activator = services.GetActivator();
105+
106+
// Act
107+
var retVal = original.DecryptElement(activator);
108+
109+
// Assert
110+
XmlAssert.Equal("<value />", retVal);
111+
mockTypeNameResolver.Verify(o => o.TryResolveType(It.IsAny<string>(), out resolvedType), Times.Once());
112+
}
113+
114+
[Fact]
115+
public void DecryptElement_KnownType_UnableToResolveType_Success()
116+
{
117+
// Arrange
118+
var decryptorTypeName = typeof(NullXmlDecryptor).AssemblyQualifiedName;
119+
120+
var original = XElement.Parse(@$"
121+
<x:encryptedSecret decryptorType='{decryptorTypeName}' xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
122+
<node>
123+
<value />
124+
</node>
125+
</x:encryptedSecret>");
126+
127+
var mockActivator = new Mock<IActivator>();
128+
mockActivator.Setup(o => o.CreateInstance(typeof(IXmlDecryptor), decryptorTypeName)).Returns(new NullXmlDecryptor());
129+
var mockTypeNameResolver = mockActivator.As<ITypeNameResolver>();
130+
Type resolvedType = null;
131+
mockTypeNameResolver.Setup(mockTypeNameResolver => mockTypeNameResolver.TryResolveType(It.IsAny<string>(), out resolvedType)).Returns(false);
132+
133+
var serviceCollection = new ServiceCollection();
134+
serviceCollection.AddSingleton<IActivator>(mockActivator.Object);
135+
var services = serviceCollection.BuildServiceProvider();
136+
var activator = services.GetActivator();
137+
138+
// Act
139+
var retVal = original.DecryptElement(activator);
140+
141+
// Assert
142+
XmlAssert.Equal("<value />", retVal);
143+
mockTypeNameResolver.Verify(o => o.TryResolveType(It.IsAny<string>(), out resolvedType), Times.Once());
144+
}
145+
52146
[Fact]
53147
public void DecryptElement_MultipleNodesRequireDecryption_AvoidsRecursion_Success()
54148
{

0 commit comments

Comments
 (0)