// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis;
namespace Microsoft.NetCore.Analyzers.Security.Helpers
{
/// <summary>
/// Determines if a given type is insecure for deserialization, by seeing if it contain known dangerous types.
/// </summary>
internal sealed partial class InsecureDeserializationTypeDecider
{
private static readonly string[] InsecureTypeNames =
{
WellKnownTypeNames.SystemDataDataSet,
WellKnownTypeNames.SystemDataDataTable,
};
private static readonly BoundedCacheWithFactory<Compilation, InsecureDeserializationTypeDecider> BoundedCache =
new();
/// <summary>
/// Gets a cached <see cref="InsecureDeserializationTypeDecider"/> for the given compilation.
/// </summary>
/// <param name="compilation">Compilation that the decider is for.</param>
/// <returns>Cached decider.</returns>
public static InsecureDeserializationTypeDecider GetOrCreate(Compilation compilation)
{
return BoundedCache.GetOrCreateValue(compilation, Create);
// Local functions.
static InsecureDeserializationTypeDecider Create(Compilation c) => new(c);
}
/// <summary>
/// Constructs.
/// </summary>
/// <param name="compilation">Compilation being analyzed.</param>
private InsecureDeserializationTypeDecider(Compilation compilation)
{
foreach (string typeName in InsecureTypeNames)
{
if (compilation.TryGetOrCreateTypeByMetadataName(typeName, out INamedTypeSymbol? namedTypeSymbol))
{
this.InsecureTypeSymbols.Add(namedTypeSymbol);
}
}
this.SymbolByDisplayStringComparer = new SymbolByDisplayStringComparer(compilation);
this.WellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(compilation);
this.GeneratedCodeAttributeTypeSymbol = this.WellKnownTypeProvider.GetOrCreateTypeByMetadataName(
WellKnownTypeNames.SystemCodeDomCompilerGeneratedCodeAttribute);
this.SerializableAttributeTypeSymbol = this.WellKnownTypeProvider.GetOrCreateTypeByMetadataName(
WellKnownTypeNames.SystemSerializableAttribute);
this.NonSerializedAttributeTypeSymbol = this.WellKnownTypeProvider.GetOrCreateTypeByMetadataName(
WellKnownTypeNames.SystemSerializableAttribute);
this.DataContractAttributeTypeSymbol = this.WellKnownTypeProvider.GetOrCreateTypeByMetadataName(
WellKnownTypeNames.SystemRuntimeSerializationDataContractAttribute);
this.DataMemberAttributeTypeSymbol = this.WellKnownTypeProvider.GetOrCreateTypeByMetadataName(
WellKnownTypeNames.SystemRuntimeSerializationDataMemberAttribute);
this.IgnoreDataMemberTypeSymbol = this.WellKnownTypeProvider.GetOrCreateTypeByMetadataName(
WellKnownTypeNames.SystemRuntimeSerializationIgnoreDataMemberAttribute);
this.KnownTypeAttributeTypeSymbol = this.WellKnownTypeProvider.GetOrCreateTypeByMetadataName(
WellKnownTypeNames.SystemRuntimeSerializationKnownTypeAttribute);
this.XmlSerializationAttributeTypes = new XmlSerializationAttributeTypes(
this.WellKnownTypeProvider);
this.JsonIgnoreAttributeTypeSymbol = this.WellKnownTypeProvider.GetOrCreateTypeByMetadataName(
WellKnownTypeNames.NewtonsoftJsonJsonIgnoreAttribute);
}
/// <summary>
/// Doesn't construct.
/// </summary>
private InsecureDeserializationTypeDecider()
{
throw new NotSupportedException();
}
/// <summary>
/// Comparer for the compilation's TypeSymbols.
/// </summary>
public SymbolByDisplayStringComparer SymbolByDisplayStringComparer { get; }
/// <summary>
/// Type cache.
/// </summary>
private WellKnownTypeProvider WellKnownTypeProvider { get; }
/// <summary>
/// Set of type symbols for types that are insecure if deserialized.
/// </summary>
private HashSet<ITypeSymbol> InsecureTypeSymbols { get; } = new HashSet<ITypeSymbol>();
private INamedTypeSymbol? GeneratedCodeAttributeTypeSymbol { get; }
private INamedTypeSymbol? SerializableAttributeTypeSymbol { get; }
private INamedTypeSymbol? NonSerializedAttributeTypeSymbol { get; }
private INamedTypeSymbol? DataContractAttributeTypeSymbol { get; }
private INamedTypeSymbol? DataMemberAttributeTypeSymbol { get; }
private INamedTypeSymbol? IgnoreDataMemberTypeSymbol { get; }
private INamedTypeSymbol? KnownTypeAttributeTypeSymbol { get; }
private XmlSerializationAttributeTypes XmlSerializationAttributeTypes { get; }
private INamedTypeSymbol? JsonIgnoreAttributeTypeSymbol { get; }
// Cache results for IsTypeInsecure()
// Key: typeSymbol in IsTypeInsecure()
// Value: insecureTypeSymbol in IsTypeInsecure()
private readonly ConcurrentDictionary<ITypeSymbol, ITypeSymbol?> IsTypeInsecureCache =
new();
/// <summary>
/// Determines if the given type is insecure when deserialized, without looking at its child fields and properties.
/// </summary>
/// <param name="typeSymbol">Type to check.</param>
/// <param name="insecureTypeSymbol">Insecure type, if the checked type is insecure.</param>
/// <returns>True if insecure, false otherwise.</returns>
/// <remarks>This only considers the type and its associated types (generic type arguments, base classes, etc), not
/// types of member fields and properties.</remarks>
public bool IsTypeInsecure(
ITypeSymbol? typeSymbol,
[NotNullWhen(returnValue: true)] out ITypeSymbol? insecureTypeSymbol)
{
insecureTypeSymbol = null;
if (typeSymbol == null || this.InsecureTypeSymbols.Count == 0)
{
return false;
}
insecureTypeSymbol = this.IsTypeInsecureCache.GetOrAdd(typeSymbol, Compute(typeSymbol));
return insecureTypeSymbol != null;
// Local functions.
ITypeSymbol? Compute(ITypeSymbol typeSymbol)
{
// Sort type symbols by display string so that we get consistent results.
SortedSet<ITypeSymbol> associatedTypeSymbols = new SortedSet<ITypeSymbol>(
this.SymbolByDisplayStringComparer);
GetAssociatedTypes(typeSymbol, associatedTypeSymbols);
foreach (ITypeSymbol t in associatedTypeSymbols)
{
if (this.InsecureTypeSymbols.Contains(t))
{
return t;
}
}
return null;
}
}
// Cache for IsObjectGraphInsecure results.
// Key: (rootType, options) arguments in IsObjectGraphInsecure()
// Value: results argument in IsObjectGraphInsecure().
private readonly ConcurrentDictionary<(ITypeSymbol, ObjectGraphOptions), ImmutableArray<InsecureObjectGraphResult>> IsObjectGraphInsecureCache =
new();
/// <summary>
/// Determines if a type's object graph contains an insecure type, by walking through its serializable members.
/// </summary>
/// <param name="rootType">Type to check.</param>
/// <param name="options">Options for the type of serialization.</param>
/// <param name="results">List to populate results of which symbols (fields or properties) are an insecure
/// type.</param>
/// <returns>True if are any insecure symbols, false otherwise.</returns>
[SuppressMessage("Style", "IDE0047:Remove unnecessary parentheses", Justification = "Group related conditions together.")]
public bool IsObjectGraphInsecure(
ITypeSymbol? rootType,
ObjectGraphOptions options,
out ImmutableArray<InsecureObjectGraphResult> results)
{
options.ThrowIfInvalid(nameof(options));
if (this.InsecureTypeSymbols.Count == 0 || rootType == null)
{
results = ImmutableArray<InsecureObjectGraphResult>.Empty;
return false;
}
results = this.IsObjectGraphInsecureCache.GetOrAdd((rootType, options), Compute);
return !results.IsEmpty;
// Local functions.
ImmutableArray<InsecureObjectGraphResult> Compute((ITypeSymbol, ObjectGraphOptions) _)
{
ImmutableArray<InsecureObjectGraphResult>.Builder resultBuilder =
ImmutableArray.CreateBuilder<InsecureObjectGraphResult>();
using PooledHashSet<ITypeSymbol> visitedTypes = PooledHashSet<ITypeSymbol>.GetInstance();
GetInsecureSymbol(rootType, visitedTypes, resultBuilder);
return resultBuilder.ToImmutable();
}
void GetInsecureSymbol(
ITypeSymbol typeSymbol,
PooledHashSet<ITypeSymbol> visitedTypes,
ImmutableArray<InsecureObjectGraphResult>.Builder resultBuilder)
{
if (!visitedTypes.Add(typeSymbol))
{
return;
}
if (this.IsTypeInsecure(typeSymbol, out ITypeSymbol? typeInsecureTypeSymbol))
{
resultBuilder.Add(new InsecureObjectGraphResult(typeSymbol, null, null, typeInsecureTypeSymbol));
}
bool[] hasAttributes = typeSymbol.HasAttributes(
this.GeneratedCodeAttributeTypeSymbol,
this.SerializableAttributeTypeSymbol,
this.DataContractAttributeTypeSymbol,
this.KnownTypeAttributeTypeSymbol);
int index = 0;
bool hasGeneratedCodeAttribute = hasAttributes[index++];
bool hasSerializableAttribute = hasAttributes[index++];
bool hasDataContractAttribute = hasAttributes[index++];
bool hasKnownTypeAttribute = hasAttributes[index++];
bool hasAnyIgnoreDataMemberAttribute =
typeSymbol.GetMembers().Any(m => m.HasAnyAttribute(this.IgnoreDataMemberTypeSymbol));
bool hasAnyXmlSerializationAttributes =
this.XmlSerializationAttributeTypes.HasAnyAttribute(typeSymbol)
|| typeSymbol.GetMembers().Any(this.XmlSerializationAttributeTypes.HasAnyAttribute);
// Consider handling other Newtonsoft Json.NET member serialization modes other than its default.
// Sort type symbols by display strings.
// Keep track of member types we see, and we'll recurse through those afterwards.
SortedSet<ITypeSymbol> typesToRecurse = new SortedSet<ITypeSymbol>(this.SymbolByDisplayStringComparer);
foreach (ISymbol member in typeSymbol.GetMembers())
{
switch (member)
{
case IFieldSymbol fieldSymbol:
if (!fieldSymbol.IsStatic
&& !fieldSymbol.IsBackingFieldForProperty(out _) // Handle properties below.
&& ((options.BinarySerialization
&& hasSerializableAttribute
&& !fieldSymbol.HasAnyAttribute(this.NonSerializedAttributeTypeSymbol))
|| (options.DataContractSerialization
&& ((hasDataContractAttribute && fieldSymbol.HasAnyAttribute(this.DataMemberAttributeTypeSymbol))
|| (!hasDataContractAttribute && !fieldSymbol.HasAnyAttribute(this.IgnoreDataMemberTypeSymbol))))
|| (options.XmlSerialization
&& !fieldSymbol.HasAnyAttribute(
this.XmlSerializationAttributeTypes.XmlIgnoreAttribute)
&& fieldSymbol.DeclaredAccessibility == Accessibility.Public)
|| (options.JavaScriptSerializer
&& fieldSymbol.DeclaredAccessibility == Accessibility.Public)
|| (options.NewtonsoftJsonNetSerialization
&& fieldSymbol.DeclaredAccessibility == Accessibility.Public
&& !fieldSymbol.HasAnyAttribute(this.JsonIgnoreAttributeTypeSymbol)
&& !fieldSymbol.HasAnyAttribute(this.NonSerializedAttributeTypeSymbol))))
{
if (this.IsTypeInsecure(fieldSymbol.Type, out ITypeSymbol? fieldInsecureTypeSymbol))
{
resultBuilder.Add(
new InsecureObjectGraphResult(
fieldSymbol,
null,
null,
fieldInsecureTypeSymbol));
}
else
{
typesToRecurse.Add(fieldSymbol.Type);
}
}
break;
case IPropertySymbol propertySymbol:
if (!propertySymbol.IsStatic
&& ((options.BinarySerialization
&& hasSerializableAttribute
&& !propertySymbol.HasAnyAttribute(this.NonSerializedAttributeTypeSymbol)
&& propertySymbol.IsPropertyWithBackingField(out _)
)
|| (options.DataContractSerialization
&& ((hasDataContractAttribute && propertySymbol.HasAnyAttribute(this.DataMemberAttributeTypeSymbol))
|| (!hasDataContractAttribute && !propertySymbol.HasAnyAttribute(this.IgnoreDataMemberTypeSymbol)))
&& propertySymbol.GetMethod != null
&& propertySymbol.SetMethod != null)
|| (options.XmlSerialization
&& !propertySymbol.HasAnyAttribute(this.XmlSerializationAttributeTypes.XmlIgnoreAttribute)
&& propertySymbol.DeclaredAccessibility == Accessibility.Public
&& propertySymbol.GetMethod != null
&& propertySymbol.GetMethod.DeclaredAccessibility == Accessibility.Public
&& propertySymbol.SetMethod != null
&& propertySymbol.SetMethod.DeclaredAccessibility == Accessibility.Public)
|| (options.JavaScriptSerializer
&& propertySymbol.DeclaredAccessibility == Accessibility.Public
&& propertySymbol.SetMethod != null
&& propertySymbol.SetMethod.DeclaredAccessibility == Accessibility.Public)
|| (options.NewtonsoftJsonNetSerialization
&& propertySymbol.DeclaredAccessibility == Accessibility.Public
&& !propertySymbol.HasAnyAttribute(this.JsonIgnoreAttributeTypeSymbol)
&& !propertySymbol.HasAnyAttribute(this.NonSerializedAttributeTypeSymbol))))
{
if (this.IsTypeInsecure(propertySymbol.Type, out ITypeSymbol? propertyInsecureTypeSymbol))
{
resultBuilder.Add(
new InsecureObjectGraphResult(
propertySymbol,
null,
null,
propertyInsecureTypeSymbol));
}
else
{
typesToRecurse.Add(propertySymbol.Type);
}
}
break;
}
}
if (options.DataContractSerialization)
{
// Look through [KnownType(typeof(Whatev))] attributes.
foreach (AttributeData knownTypeAttributeData in typeSymbol.GetAttributes(this.KnownTypeAttributeTypeSymbol))
{
if (knownTypeAttributeData.AttributeConstructor?.Parameters.Length != 1
|| knownTypeAttributeData.ConstructorArguments.Length != 1)
{
continue;
}
var typedConstant = knownTypeAttributeData.ConstructorArguments[0];
if (typedConstant.Kind != TypedConstantKind.Type // Not handling the string methodName overload
|| typedConstant.Value is not ITypeSymbol typedConstantTypeSymbol)
{
continue;
}
if (this.IsTypeInsecure(typedConstantTypeSymbol, out ITypeSymbol? knownTypeInsecureType))
{
resultBuilder.Add(
new InsecureObjectGraphResult(
null,
knownTypeAttributeData,
typedConstant,
knownTypeInsecureType));
}
else
{
typesToRecurse.Add(typedConstantTypeSymbol);
}
}
}
if (options.XmlSerialization)
{
// Look through [XmlInclude(typeof(Whatev))] attributes.
foreach (AttributeData xmlIncludeAttributeData
in typeSymbol.GetAttributes(this.XmlSerializationAttributeTypes.XmlIncludeAttribute))
{
if (xmlIncludeAttributeData.AttributeConstructor?.Parameters.Length != 1
|| xmlIncludeAttributeData.ConstructorArguments.Length != 1)
{
continue;
}
var typedConstant = xmlIncludeAttributeData.ConstructorArguments[0];
if (typedConstant.Kind != TypedConstantKind.Type
|| typedConstant.Value is not ITypeSymbol typedConstantTypeSymbol)
{
continue;
}
if (this.IsTypeInsecure(typedConstantTypeSymbol, out ITypeSymbol? xmlIncludeInsecureType))
{
resultBuilder.Add(
new InsecureObjectGraphResult(
null,
xmlIncludeAttributeData,
typedConstant,
xmlIncludeInsecureType));
}
else
{
typesToRecurse.Add(typedConstantTypeSymbol);
}
}
}
if (options.Recurse)
{
foreach (ITypeSymbol memberTypeSymbol in typesToRecurse)
{
GetInsecureSymbol(memberTypeSymbol, visitedTypes, resultBuilder);
}
}
}
}
/// <summary>
/// Gets "associated" types, e.g. "List<Foo<Bar[]>>" means "List<T>", "Foo<T>", and "Bar".
/// </summary>
/// <param name="type">Type to get associated types for.</param>
/// <param name="results">Set to populate with associated types.</param>
private static void GetAssociatedTypes(
ITypeSymbol? type,
SortedSet<ITypeSymbol> results)
{
if (type == null || !results.Add(type))
{
return;
}
if (type is INamedTypeSymbol namedTypeSymbol)
{
// 1. Type arguments of generic type.
if (namedTypeSymbol.IsGenericType)
{
foreach (ITypeSymbol? arg in namedTypeSymbol.TypeArguments)
{
GetAssociatedTypes(arg, results);
}
}
// 2. The type it constructed from.
GetAssociatedTypes(namedTypeSymbol.ConstructedFrom, results);
}
else if (type is IArrayTypeSymbol arrayTypeSymbol)
{
// 3. Element type of the array.
GetAssociatedTypes(arrayTypeSymbol.ElementType, results);
}
// 4. Base type.
GetAssociatedTypes(type.BaseType, results);
}
}
}