// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.FlowAnalysis.Analysis.PropertySetAnalysis;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.PointsToAnalysis;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.ValueContentAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.NetCore.Analyzers.Security.Helpers;
namespace Microsoft.NetCore.Analyzers.Security
{
using static MicrosoftNetCoreAnalyzersResources;
/// <summary>
/// CA2329: <inheritdoc cref="JsonNetInsecureSerializerTitle"/>
/// CA2330: <inheritdoc cref="JsonNetMaybeInsecureSerializerTitle"/>
/// For detecting deserialization with <see cref="T:Newtonsoft.Json.JsonSerializer"/> when its TypeNameHandling != None
/// and its Binder/SerializationBinder is null.
/// </summary>
[SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "The comment references a type that is not referenced by this compilation.")]
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
internal class DoNotUseInsecureDeserializerJsonNetWithoutBinder : DiagnosticAnalyzer
{
internal static readonly DiagnosticDescriptor DefinitelyInsecureSerializer =
SecurityHelpers.CreateDiagnosticDescriptor(
"CA2329",
nameof(JsonNetInsecureSerializerTitle),
nameof(JsonNetInsecureSerializerMessage),
RuleLevel.Disabled,
isPortedFxCopRule: false,
isDataflowRule: true,
isReportedAtCompilationEnd: true);
internal static readonly DiagnosticDescriptor MaybeInsecureSerializer =
SecurityHelpers.CreateDiagnosticDescriptor(
"CA2330",
nameof(JsonNetMaybeInsecureSerializerTitle),
nameof(JsonNetMaybeInsecureSerializerMessage),
RuleLevel.Disabled,
isPortedFxCopRule: false,
isDataflowRule: true,
isReportedAtCompilationEnd: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
DefinitelyInsecureSerializer,
MaybeInsecureSerializer);
/// <summary>
/// PropertySetAbstractValue index for TypeNameHandling property.
/// </summary>
private const int TypeNameHandlingIndex = 0;
/// <summary>
/// PropertySetAbstractValue index for Binder / SerializationBinder properties (both are aliased to same underlying value).
/// </summary>
private const int SerializationBinderIndex = 1;
private static readonly ConstructorMapper ConstructorMapper = new(
(IMethodSymbol constructorMethod, IReadOnlyList<PointsToAbstractValue> argumentPointsToAbstractValues) =>
{
if (constructorMethod.Parameters.IsEmpty)
{
return PropertySetAbstractValue.GetInstance(
PropertySetAbstractValueKind.Unflagged, // TypeNameHandling defaults to None.
PropertySetAbstractValueKind.Flagged); // Binder / SerializationBinder defaults to null.
}
else
{
Debug.Fail($"Unhandled JsonSerializer constructor {constructorMethod.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}");
return PropertySetAbstractValue.GetInstance(
PropertySetAbstractValueKind.Unflagged,
PropertySetAbstractValueKind.Unflagged);
}
});
private static readonly PropertyMapperCollection PropertyMappers = new(
new PropertyMapper(
"TypeNameHandling",
(ValueContentAbstractValue valueContentAbstractValue) =>
PropertySetCallbacks.EvaluateLiteralValues(
valueContentAbstractValue,
(object? o) => o is int i && i != 0), // None is 0, and anything other than None is flagged.
TypeNameHandlingIndex),
new PropertyMapper(
"Binder",
PropertySetCallbacks.FlagIfNull,
SerializationBinderIndex), // Binder & SerializationBinder have the same underlying value.
new PropertyMapper(
"SerializationBinder",
PropertySetCallbacks.FlagIfNull,
SerializationBinderIndex)); // Binder & SerializationBinder have the same underlying value.
private static readonly HazardousUsageEvaluatorCollection HazardousUsageEvaluators = new(
SecurityHelpers.JsonSerializerDeserializationMethods.Select(
(string methodName) => new HazardousUsageEvaluator(
methodName,
PropertySetCallbacks.HazardousIfAllFlaggedAndAtLeastOneKnown))
.Concat(
new HazardousUsageEvaluator(
HazardousUsageEvaluatorKind.Initialization,
PropertySetCallbacks.HazardousIfAllFlaggedAndAtLeastOneKnown)));
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
// Security analyzer - analyze and report diagnostics on generated code.
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.RegisterCompilationStartAction(
(CompilationStartAnalysisContext compilationStartAnalysisContext) =>
{
if (!compilationStartAnalysisContext.Compilation.TryGetOrCreateTypeByMetadataName(
WellKnownTypeNames.NewtonsoftJsonJsonSerializer,
out INamedTypeSymbol? jsonSerializerSymbol))
{
return;
}
PooledHashSet<(IOperation Operation, ISymbol ContainingSymbol)> rootOperationsNeedingAnalysis = PooledHashSet<(IOperation, ISymbol)>.GetInstance();
compilationStartAnalysisContext.RegisterOperationBlockStartAction(
(OperationBlockStartAnalysisContext operationBlockStartAnalysisContext) =>
{
ISymbol owningSymbol = operationBlockStartAnalysisContext.OwningSymbol;
// TODO: Handle case when exactly one of the below rules is configured to skip analysis.
if (operationBlockStartAnalysisContext.Options.IsConfiguredToSkipAnalysis(
DefinitelyInsecureSerializer,
owningSymbol,
operationBlockStartAnalysisContext.Compilation)
&& operationBlockStartAnalysisContext.Options.IsConfiguredToSkipAnalysis(
MaybeInsecureSerializer,
owningSymbol,
operationBlockStartAnalysisContext.Compilation))
{
return;
}
operationBlockStartAnalysisContext.RegisterOperationAction(
(OperationAnalysisContext operationAnalysisContext) =>
{
IInvocationOperation invocationOperation =
(IInvocationOperation)operationAnalysisContext.Operation;
if (jsonSerializerSymbol.Equals(invocationOperation.TargetMethod.ContainingType)
&& SecurityHelpers.JsonSerializerDeserializationMethods.Contains(
invocationOperation.TargetMethod.Name))
{
lock (rootOperationsNeedingAnalysis)
{
rootOperationsNeedingAnalysis.Add(
(invocationOperation.GetRoot(), operationAnalysisContext.ContainingSymbol));
}
}
},
OperationKind.Invocation);
operationBlockStartAnalysisContext.RegisterOperationAction(
(OperationAnalysisContext operationAnalysisContext) =>
{
IObjectCreationOperation objectCreationOperation =
(IObjectCreationOperation)operationAnalysisContext.Operation;
if (jsonSerializerSymbol.Equals(objectCreationOperation.Type))
{
lock (rootOperationsNeedingAnalysis)
{
rootOperationsNeedingAnalysis.Add(
(objectCreationOperation.GetRoot(), operationAnalysisContext.ContainingSymbol));
}
}
},
OperationKind.ObjectCreation);
});
compilationStartAnalysisContext.RegisterCompilationEndAction(
(CompilationAnalysisContext compilationAnalysisContext) =>
{
PooledDictionary<(Location Location, IMethodSymbol? Method), HazardousUsageEvaluationResult>? allResults = null;
try
{
lock (rootOperationsNeedingAnalysis)
{
if (!rootOperationsNeedingAnalysis.Any())
{
return;
}
allResults = PropertySetAnalysis.BatchGetOrComputeHazardousUsages(
compilationAnalysisContext.Compilation,
rootOperationsNeedingAnalysis,
compilationAnalysisContext.Options,
WellKnownTypeNames.NewtonsoftJsonJsonSerializer,
ConstructorMapper,
PropertyMappers,
HazardousUsageEvaluators,
InterproceduralAnalysisConfiguration.Create(
compilationAnalysisContext.Options,
SupportedDiagnostics,
rootOperationsNeedingAnalysis.First().Operation,
compilationAnalysisContext.Compilation,
defaultInterproceduralAnalysisKind: InterproceduralAnalysisKind.ContextSensitive));
}
if (allResults == null)
{
return;
}
foreach (KeyValuePair<(Location Location, IMethodSymbol? Method), HazardousUsageEvaluationResult> kvp
in allResults)
{
DiagnosticDescriptor descriptor;
switch (kvp.Value)
{
case HazardousUsageEvaluationResult.Flagged:
descriptor = DefinitelyInsecureSerializer;
break;
case HazardousUsageEvaluationResult.MaybeFlagged:
descriptor = MaybeInsecureSerializer;
break;
default:
Debug.Fail($"Unhandled result value {kvp.Value}");
continue;
}
compilationAnalysisContext.ReportDiagnostic(
Diagnostic.Create(
descriptor,
kvp.Key.Location));
}
}
finally
{
rootOperationsNeedingAnalysis.Free(compilationAnalysisContext.CancellationToken);
allResults?.Free(compilationAnalysisContext.CancellationToken);
}
});
});
}
}
}