File: Microsoft.NetCore.Analyzers\Security\DoNotUseInsecureSettingsForJsonNet.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// 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>
    /// CA2327: <inheritdoc cref="JsonNetInsecureSettingsTitle"/>
    /// CA2328: <inheritdoc cref="JsonNetMaybeInsecureSettingsTitle"/>
    /// For detecting potentially insecure deserialization settings with <see cref="T:Newtonsoft.Json.JsonSerializerSettings"/>.
    /// </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)]
    public sealed class DoNotUseInsecureSettingsForJsonNet : DiagnosticAnalyzer
    {
        internal static readonly DiagnosticDescriptor DefinitelyInsecureSettings =
            SecurityHelpers.CreateDiagnosticDescriptor(
                "CA2327",
                nameof(JsonNetInsecureSettingsTitle),
                nameof(JsonNetInsecureSettingsMessage),
                RuleLevel.Disabled,
                isPortedFxCopRule: false,
                isDataflowRule: true,
                isReportedAtCompilationEnd: true);

        internal static readonly DiagnosticDescriptor MaybeInsecureSettings =
            SecurityHelpers.CreateDiagnosticDescriptor(
                "CA2328",
                nameof(JsonNetMaybeInsecureSettingsTitle),
                nameof(JsonNetMaybeInsecureSettingsMessage),
                RuleLevel.Disabled,
                isPortedFxCopRule: false,
                isDataflowRule: true,
                isReportedAtCompilationEnd: true);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
            ImmutableArray.Create(
                DefinitelyInsecureSettings,
                MaybeInsecureSettings);

        /// <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 JsonSerializerSettings 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.JsonSerializerInstantiateWithSettingsMethods.Select(
                (string methodName) => new HazardousUsageEvaluator(
                    WellKnownTypeNames.NewtonsoftJsonJsonSerializer,
                    methodName,
                    "settings",
                    PropertySetCallbacks.HazardousIfAllFlaggedAndAtLeastOneKnown))
                .Concat(
                    SecurityHelpers.JsonConvertWithSettingsMethods.Select(
                        (string methodName) => new HazardousUsageEvaluator(
                            WellKnownTypeNames.NewtonsoftJsonJsonConvert,
                            methodName,
                            "settings",
                            PropertySetCallbacks.HazardousIfAllFlaggedAndAtLeastOneKnown)))
                .Concat(
                    new HazardousUsageEvaluator(
                        HazardousUsageEvaluatorKind.Return,
                        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) =>
                {
                    WellKnownTypeProvider wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(compilationStartAnalysisContext.Compilation);
                    if (!wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.NewtonsoftJsonJsonSerializerSettings, out INamedTypeSymbol? jsonSerializerSettingsSymbol)
                        || !wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.NewtonsoftJsonJsonSerializer, out INamedTypeSymbol? jsonSerializerSymbol)
                        || !wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.NewtonsoftJsonJsonConvert, out INamedTypeSymbol? jsonConvertSymbol))
                    {
                        return;
                    }

                    PooledHashSet<(IOperation Operation, ISymbol ContainingSymbol)> rootOperationsNeedingAnalysis = PooledHashSet<(IOperation, ISymbol)>.GetInstance();

                    compilationStartAnalysisContext.RegisterOperationBlockStartAction(
                        (OperationBlockStartAnalysisContext operationBlockStartAnalysisContext) =>
                        {
                            var owningSymbol = operationBlockStartAnalysisContext.OwningSymbol;

                            // TODO: Handle case when exactly one of the below rules is configured to skip analysis.
                            if (operationBlockStartAnalysisContext.Options.IsConfiguredToSkipAnalysis(DefinitelyInsecureSettings,
                                    owningSymbol, operationBlockStartAnalysisContext.Compilation) &&
                                operationBlockStartAnalysisContext.Options.IsConfiguredToSkipAnalysis(MaybeInsecureSettings,
                                    owningSymbol, operationBlockStartAnalysisContext.Compilation))
                            {
                                return;
                            }

                            operationBlockStartAnalysisContext.RegisterOperationAction(
                                (OperationAnalysisContext operationAnalysisContext) =>
                                {
                                    IInvocationOperation invocationOperation =
                                        (IInvocationOperation)operationAnalysisContext.Operation;
                                    if ((jsonSerializerSymbol.Equals(invocationOperation.TargetMethod.ContainingType)
                                            && SecurityHelpers.JsonSerializerInstantiateWithSettingsMethods.Contains(invocationOperation.TargetMethod.Name)
                                            && invocationOperation.TargetMethod.Parameters.Any(
                                                p => jsonSerializerSettingsSymbol.Equals(p.Type)))
                                        || (jsonConvertSymbol.Equals(invocationOperation.TargetMethod.ContainingType)
                                                && SecurityHelpers.JsonSerializerInstantiateWithSettingsMethods.Contains(invocationOperation.TargetMethod.Name)
                                                && invocationOperation.TargetMethod.Parameters.Any(
                                                    p => jsonSerializerSettingsSymbol.Equals(p.Type)))
                                        || jsonSerializerSettingsSymbol.Equals(invocationOperation.TargetMethod.ReturnType))
                                    {
                                        lock (rootOperationsNeedingAnalysis)
                                        {
                                            rootOperationsNeedingAnalysis.Add(
                                                (invocationOperation.GetRoot(), operationAnalysisContext.ContainingSymbol));
                                        }
                                    }
                                },
                                OperationKind.Invocation);

                            operationBlockStartAnalysisContext.RegisterOperationAction(
                                (OperationAnalysisContext operationAnalysisContext) =>
                                {
                                    IObjectCreationOperation objectCreationOperation =
                                        (IObjectCreationOperation)operationAnalysisContext.Operation;
                                    if (jsonSerializerSettingsSymbol.Equals(objectCreationOperation.Type))
                                    {
                                        lock (rootOperationsNeedingAnalysis)
                                        {
                                            rootOperationsNeedingAnalysis.Add(
                                                (objectCreationOperation.GetRoot(), operationAnalysisContext.ContainingSymbol));
                                        }
                                    }
                                },
                                OperationKind.ObjectCreation);

                            operationBlockStartAnalysisContext.RegisterOperationAction(
                                (OperationAnalysisContext operationAnalysisContext) =>
                                {
                                    IReturnOperation returnOperation = (IReturnOperation)operationAnalysisContext.Operation;
                                    if (jsonSerializerSettingsSymbol.Equals(returnOperation.Type))
                                    {
                                        rootOperationsNeedingAnalysis.Add(
                                            (returnOperation.GetRoot(), operationAnalysisContext.ContainingSymbol));
                                    }
                                },
                                OperationKind.Return);
                        });

                    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.NewtonsoftJsonJsonSerializerSettings,
                                        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 = DefinitelyInsecureSettings;
                                            break;

                                        case HazardousUsageEvaluationResult.MaybeFlagged:
                                            descriptor = MaybeInsecureSettings;
                                            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);
                            }
                        });
                });
        }
    }
}