// 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;
}
}
}
}
}