File: Microsoft.NetCore.Analyzers\Data\ReviewSqlQueriesForSecurityVulnerabilities.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;
using System.Collections.Immutable;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.ValueContentAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.PointsToAnalysis;

namespace Microsoft.NetCore.Analyzers.Data
{
    using static MicrosoftNetCoreAnalyzersResources;

    /// <summary>
    /// CA2100: <inheritdoc cref="ReviewSQLQueriesForSecurityVulnerabilitiesTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class ReviewSqlQueriesForSecurityVulnerabilities : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA2100";

        internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(ReviewSQLQueriesForSecurityVulnerabilitiesTitle)),
            CreateLocalizableResourceString(nameof(ReviewSQLQueriesForSecurityVulnerabilitiesMessageNoNonLiterals)),
            DiagnosticCategory.Security,
            RuleLevel.Disabled,
            description: CreateLocalizableResourceString(nameof(ReviewSQLQueriesForSecurityVulnerabilitiesDescription)),
            isPortedFxCopRule: true,
            isDataflowRule: true);

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

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
            context.RegisterCompilationStartAction(compilationContext =>
            {
                INamedTypeSymbol? iDbCommandType = compilationContext.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDataIDbCommand);
                INamedTypeSymbol? iDataAdapterType = compilationContext.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDataIDataAdapter);
                IPropertySymbol? commandTextProperty = iDbCommandType?.GetMembers("CommandText").OfType<IPropertySymbol>().FirstOrDefault();

                if (iDbCommandType == null ||
                    iDataAdapterType == null ||
                    commandTextProperty == null)
                {
                    return;
                }

                compilationContext.RegisterOperationBlockStartAction(operationBlockStartContext =>
                {
                    ISymbol symbol = operationBlockStartContext.OwningSymbol;
                    var isInDbCommandConstructor = false;
                    var isInDataAdapterConstructor = false;

                    if (symbol.Kind != SymbolKind.Method)
                    {
                        return;
                    }

                    var methodSymbol = (IMethodSymbol)symbol;

                    if (methodSymbol.MethodKind == MethodKind.Constructor)
                    {
                        CheckForDbCommandAndDataAdapterImplementation(symbol.ContainingType, iDbCommandType, iDataAdapterType, out isInDbCommandConstructor, out isInDataAdapterConstructor);
                    }

                    operationBlockStartContext.RegisterOperationAction(operationContext =>
                    {
                        var creation = (IObjectCreationOperation)operationContext.Operation;
                        AnalyzeMethodCall(operationContext, creation.Constructor, symbol, creation.Arguments, creation.Syntax, isInDbCommandConstructor, isInDataAdapterConstructor, iDbCommandType, iDataAdapterType);
                    }, OperationKind.ObjectCreation);

                    // If an object calls a constructor in a base class or the same class, this will get called.
                    operationBlockStartContext.RegisterOperationAction(operationContext =>
                    {
                        var invocation = (IInvocationOperation)operationContext.Operation;

                        // We only analyze constructor invocations
                        if (invocation.TargetMethod.MethodKind != MethodKind.Constructor)
                        {
                            return;
                        }

                        // If we're calling another constructor in the same class from this constructor, assume that all parameters are safe and skip analysis. Parameter usage
                        // will be analyzed there
                        if (Equals(invocation.TargetMethod.ContainingType, symbol.ContainingType))
                        {
                            return;
                        }

                        AnalyzeMethodCall(operationContext, invocation.TargetMethod, symbol, invocation.Arguments, invocation.Syntax, isInDbCommandConstructor, isInDataAdapterConstructor, iDbCommandType, iDataAdapterType);
                    }, OperationKind.Invocation);

                    operationBlockStartContext.RegisterOperationAction(operationContext =>
                    {
                        var propertyReference = (IPropertyReferenceOperation)operationContext.Operation;

                        // We're only interested in implementations of IDbCommand.CommandText
                        if (!propertyReference.Property.IsOverrideOrImplementationOfInterfaceMember(commandTextProperty))
                        {
                            return;
                        }

                        // Make sure we're in assignment statement
                        if (propertyReference.Parent is not IAssignmentOperation assignment)
                        {
                            return;
                        }

                        // Only if the property reference is actually the target of the assignment
                        if (assignment.Target != propertyReference)
                        {
                            return;
                        }

                        ReportDiagnosticIfNecessary(operationContext, assignment.Value, assignment.Syntax, propertyReference.Property, symbol);
                    }, OperationKind.PropertyReference);
                });
            });
        }

        private static void AnalyzeMethodCall(OperationAnalysisContext operationContext,
                                       IMethodSymbol? constructorSymbol,
                                       ISymbol containingSymbol,
                                       ImmutableArray<IArgumentOperation> arguments,
                                       SyntaxNode invocationSyntax,
                                       bool isInDbCommandConstructor,
                                       bool isInDataAdapterConstructor,
                                       INamedTypeSymbol iDbCommandType,
                                       INamedTypeSymbol iDataAdapterType)
        {
            if (constructorSymbol == null)
            {
                return;
            }

            CheckForDbCommandAndDataAdapterImplementation(constructorSymbol.ContainingType, iDbCommandType, iDataAdapterType,
                                                          out var callingDbCommandConstructor,
                                                          out var callingDataAdapterConstructor);

            if (!callingDataAdapterConstructor && !callingDbCommandConstructor)
            {
                return;
            }

            // All parameters the function takes that are explicit strings are potential vulnerabilities
            var potentials = arguments.WhereAsArray(arg => arg.Parameter?.Type.SpecialType == SpecialType.System_String && !arg.Parameter.IsImplicitlyDeclared);
            if (potentials.IsEmpty)
            {
                return;
            }

            var vulnerableArgumentsBuilder = ImmutableArray.CreateBuilder<IArgumentOperation>();

            foreach (var argument in potentials)
            {
                // For the constructor of a IDbCommand-derived class, if there is only one string parameter, then we just
                // assume that it's the command text. If it takes more than one string, then we need to figure out which
                // one is the command string. However, for the constructor of a IDataAdapter, a lot of times the
                // constructor also take in the connection string, so we can't assume it's the command if there is only one
                // string.
                if (callingDataAdapterConstructor || potentials.Length > 1)
                {
                    if (!IsParameterSymbolVulnerable(argument.Parameter!))
                    {
                        continue;
                    }
                }

                vulnerableArgumentsBuilder.Add(argument);
            }

            var vulnerableArguments = vulnerableArgumentsBuilder.ToImmutable();

            foreach (var argument in vulnerableArguments)
            {
                if (IsParameterSymbolVulnerable(argument.Parameter!) && (isInDbCommandConstructor || isInDataAdapterConstructor))
                {
                    //No warnings, as Constructor parameters in derived classes are assumed to be safe since this rule will check the constructor arguments at their call sites.
                    return;
                }

                if (ReportDiagnosticIfNecessary(operationContext, argument.Value, invocationSyntax, constructorSymbol, containingSymbol))
                {
                    // Only report one warning per invocation
                    return;
                }
            }
        }

        private static bool IsParameterSymbolVulnerable(IParameterSymbol parameter)
        {
            // Parameters might be vulnerable if "cmd" or "command" is in the name
            return parameter != null &&
                   (parameter.Name.IndexOf("cmd", StringComparison.OrdinalIgnoreCase) != -1 ||
                    parameter.Name.IndexOf("command", StringComparison.OrdinalIgnoreCase) != -1);
        }

        private static bool ReportDiagnosticIfNecessary(OperationAnalysisContext operationContext,
                                                 IOperation argumentValue,
                                                 SyntaxNode syntax,
                                                 ISymbol invokedSymbol,
                                                 ISymbol containingMethod)
        {
            if (operationContext.Options.IsConfiguredToSkipAnalysis(Rule, containingMethod, operationContext.Compilation) ||
                argumentValue.Type == null)
            {
                return false;
            }

            if (argumentValue.Type.SpecialType != SpecialType.System_String || !argumentValue.ConstantValue.HasValue)
            {
                // We have a candidate for diagnostic. perform more precise dataflow analysis.
                if (argumentValue.TryGetEnclosingControlFlowGraph(out var cfg))
                {
                    var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(operationContext.Compilation);
                    var valueContentResult = ValueContentAnalysis.TryGetOrComputeResult(cfg, containingMethod, wellKnownTypeProvider,
                        operationContext.Options, Rule, PointsToAnalysisKind.Complete);
                    if (valueContentResult != null)
                    {
                        ValueContentAbstractValue value = valueContentResult[argumentValue.Kind, argumentValue.Syntax];
                        if (value.NonLiteralState == ValueContainsNonLiteralState.No)
                        {
                            // The value is a constant literal or default/unitialized, so avoid flagging this usage.
                            return false;
                        }
                    }
                }

                // Review if the symbol passed to {invocation} in {method/field/constructor/etc} has user input.
                operationContext.ReportDiagnostic(syntax.CreateDiagnostic(Rule,
                                                                          invokedSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
                                                                          containingMethod.Name));
                return true;
            }

            return false;
        }

        private static void CheckForDbCommandAndDataAdapterImplementation(INamedTypeSymbol containingType,
                                                                          INamedTypeSymbol iDbCommandType,
                                                                          INamedTypeSymbol iDataAdapterType,
                                                                          out bool implementsDbCommand,
                                                                          out bool implementsDataCommand)
        {
            implementsDbCommand = false;
            implementsDataCommand = false;
            foreach (var @interface in containingType.AllInterfaces)
            {
                if (Equals(@interface, iDbCommandType))
                {
                    implementsDbCommand = true;
                }
                else if (Equals(@interface, iDataAdapterType))
                {
                    implementsDataCommand = true;
                }
            }
        }
    }
}