diff --git a/analyzers/rspec/cs/S6932.html b/analyzers/rspec/cs/S6932.html
new file mode 100644
index 00000000000..7f0cef39296
--- /dev/null
+++ b/analyzers/rspec/cs/S6932.html
@@ -0,0 +1,336 @@
+
The HttpRequest
class provides access to the raw request data through the QueryString
, Headers
, and
+Forms
properties. However, whenever possible it is recommended to use model binding instead of directly accessing the input data.
+Why is this an issue?
+Both ASP.Net MVC implementations - Core and Framework - support model binding in a comparable fashion. Model binding streamlines the
+process by automatically aligning data from HTTP requests with action method parameters, providing numerous benefits compared to manually parsing raw
+incoming request data:
+
+
+ Simplicity
+
+
+ Model binding simplifies the code by automatically mapping data from HTTP requests to action method parameters. You don’t need to write any
+ code to manually extract values from the request.
+
+
+ Type Safety
+
+
+ Model binding provides type safety by automatically converting the incoming data into the appropriate .NET types. If the conversion fails, the
+ model state becomes invalid, which you can easily check using ModelState.IsValid
.
+
+
+ Validation
+
+
+ With model binding, you can easily apply validation rules to your models using data annotations. If the incoming data doesn’t comply with these
+ rules, the model state becomes invalid.
+
+
+ Security
+
+
+ Model binding helps protect against over-posting attacks by only including properties in the model that you explicitly bind using the
+ [Bind]
attribute or by using view models that only contain the properties you want to update.
+
+
+ Maintainability
+
+
+ By using model binding, your code becomes cleaner, easier to read, and maintain. It promotes the use of strongly typed views, which can provide
+ compile-time checking of your views.
+
+
+How to fix it in ASP.NET Core
+Request.Form
, Request.Form.Files
, Request.Headers
, Request.Query
and Request.RouteValues
are keyed
+collections that expose data from the incoming HTTP request:
+
+Model binding can bind these keyed collections to
+
+ action method parameters by matching the key to the parameter name or
+ the property of a complex type by matching the key to the property name.
+
+To replace the keyed collection access, you can:
+
+The Model Binding in ASP.NET Core article describes the
+mechanisms, conventions, and customization options for model binding in more detail. Route-based binding is described in the Routing to controller actions in ASP.NET Core document.
+Code examples
+Noncompliant code example
+
+public IActionResult Post()
+{
+ var name = Request.Form["name"]; // Noncompliant: Request.Form
+ var birthdate = DateTime.Parse(Request.Form["Birthdate"]); // Noncompliant: Request.Form
+
+ var origin = Request.Headers[HeaderNames.Origin]; // Noncompliant: Request.Headers
+ var locale = Request.Query.TryGetValue("locale", out var locales)
+ ? locales.ToString()
+ : "en-US"; // Noncompliant: Request.Query
+ // ..
+}
+
+Compliant solution
+
+public record User
+{
+ [Required, StringLength(100)]
+ public required string Name { get; init; }
+ [DataType(DataType.Date)]
+ public DateTime? Birthdate { get; init; }
+}
+
+public IActionResult Post(User user, [FromHeader] string origin, [FromQuery] string locale = "en-US")
+{
+ if (ModelState.IsValid)
+ {
+ // ...
+ }
+}
+
+How does this work?
+Model binding in ASP.NET Core MVC and ASP.NET MVC 4.x works by automatically mapping data from HTTP requests to action method parameters. Here’s a
+step-by-step breakdown of how it works:
+
+ Request Data When a user submits a form or sends a request to an ASP.NET application, the request data might include form
+ data, query string parameters, request body, and HTTP headers.
+ Model Binder The model binder’s job is to create .NET objects from the request data. It looks at each parameter in the action
+ method and attempts to populate it with the incoming data.
+ Value Providers The model binder uses Value Providers to get data from various parts of the request, such as the query string,
+ form data, or route data. Each value provider tells the model binder where to find values in the request.
+ Binding The model binder tries to match the keys from the incoming data with the properties of the action method’s parameters.
+ If a match is found, it attempts to convert the incoming data into the appropriate .NET type and assigns it to the parameter.
+ Validation If the model binder can’t convert the value or if the converted value doesn’t pass any specified validation rules,
+ it adds an error to the ModelState.Errors
collection. You can check ModelState.IsValid
in your action method to see if any
+ errors occurred during model binding.
+ Action Method Execution The action method is executed with the bound parameters. If ModelState.IsValid
is
+ false
, you can handle the errors in your action method and return an appropriate response.
+
+See the links in the Resources section for more information.
+How to fix it in ASP.NET MVC 4.x
+Request.Form
and Request.QueryString
are keyed collections
+that expose data from the incoming HTTP request:
+
+Model binding can bind these keyed collections to
+
+ action method parameters by matching the key to the parameter name or
+ the property of a complex type by matching the key to the property name.
+
+To replace the keyed collection access, you can:
+
+
+
+
+
+
+
+
+ Replace
+ with parameter binding
+ or complex type binding
+
+
+
+
+ Request.Form ["id"]
+ optional [Bind]
attribute on the
+ parameter or a FormCollection
+ parameter
+ optional [Bind]
attribute on the
+ parameter or type
+
+
+ Request.QueryString ["id"]
+ optional [Bind]
attribute on the
+ parameter
+ property name must match query parameter key
+
+
+
+Code examples
+Noncompliant code example
+
+public ActionResult Post()
+{
+ var name = Request.Form["name"]; // Noncompliant: Request.Form
+ Debug.WriteLine(Request.Form[0]); // Compliant: Binding by index is not supported.
+ var birthdate = DateTime.Parse(Request.Form["Birthdate"]); // Noncompliant: Request.Form
+
+ var cultureName = Request.QueryString["locale"] ?? "en-US"; // Noncompliant: Request.QueryString
+ // ..
+}
+
+Compliant solution
+
+public class User
+{
+ [Required, StringLength(100)]
+ public string Name { get; set; }
+ [DataType(DataType.Date)]
+ public DateTime? Birthdate { get; set; }
+}
+
+public ActionResult Post(User user, [Bind(Prefix = "locale")] string cultureName = "en-US")
+{
+ if (ModelState.IsValid)
+ {
+ // ...
+ }
+}
+
+How does this work?
+Model binding in ASP.NET Core MVC and ASP.NET MVC 4.x works by automatically mapping data from HTTP requests to action method parameters. Here’s a
+step-by-step breakdown of how it works:
+
+ Request Data When a user submits a form or sends a request to an ASP.NET application, the request data might include form
+ data, query string parameters, request body, and HTTP headers.
+ Model Binder The model binder’s job is to create .NET objects from the request data. It looks at each parameter in the action
+ method and attempts to populate it with the incoming data.
+ Value Providers The model binder uses Value Providers to get data from various parts of the request, such as the query string,
+ form data, or route data. Each value provider tells the model binder where to find values in the request.
+ Binding The model binder tries to match the keys from the incoming data with the properties of the action method’s parameters.
+ If a match is found, it attempts to convert the incoming data into the appropriate .NET type and assigns it to the parameter.
+ Validation If the model binder can’t convert the value or if the converted value doesn’t pass any specified validation rules,
+ it adds an error to the ModelState.Errors
collection. You can check ModelState.IsValid
in your action method to see if any
+ errors occurred during model binding.
+ Action Method Execution The action method is executed with the bound parameters. If ModelState.IsValid
is
+ false
, you can handle the errors in your action method and return an appropriate response.
+
+See the links in the Resources section for more information.
+Resources
+Documentation
+
+
diff --git a/analyzers/rspec/cs/S6932.json b/analyzers/rspec/cs/S6932.json
new file mode 100644
index 00000000000..24c4c6e7f45
--- /dev/null
+++ b/analyzers/rspec/cs/S6932.json
@@ -0,0 +1,25 @@
+{
+ "title": "Use model binding instead of reading raw request data",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5min"
+ },
+ "tags": [
+ "asp.net"
+ ],
+ "defaultSeverity": "Major",
+ "ruleSpecification": "RSPEC-6932",
+ "sqKey": "S6932",
+ "scope": "Main",
+ "quickfix": "infeasible",
+ "code": {
+ "impacts": {
+ "MAINTAINABILITY": "HIGH",
+ "RELIABILITY": "MEDIUM",
+ "SECURITY": "MEDIUM"
+ },
+ "attribute": "FOCUSED"
+ }
+}
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs
new file mode 100644
index 00000000000..36d58d5889b
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs
@@ -0,0 +1,261 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2024 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using System.Collections.Concurrent;
+
+namespace SonarAnalyzer.Rules.CSharp;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class UseAspNetModelBinding : SonarDiagnosticAnalyzer
+{
+ private const string DiagnosticId = "S6932";
+ private const string UseAspNetModelBindingMessage = "Use model binding instead of accessing the raw request data";
+ private const string UseIFormFileBindingMessage = "Use IFormFile or IFormFileCollection binding instead";
+
+ protected override string MessageFormat => "{0}";
+
+ protected override ILanguageFacade Language => CSharpFacade.Instance;
+
+ public UseAspNetModelBinding() : base(DiagnosticId) { }
+
+ protected override void Initialize(SonarAnalysisContext context)
+ {
+ context.RegisterCompilationStartAction(compilationStartContext =>
+ {
+ var (argumentDescriptors, propertyAccessDescriptors) = GetDescriptors(compilationStartContext.Compilation);
+ if (argumentDescriptors.Any() || propertyAccessDescriptors.Any())
+ {
+ compilationStartContext.RegisterSymbolStartAction(symbolStart =>
+ {
+ // If the user overrides any action filters, model binding may not be working as expected.
+ // Then we do not want to raise on expressions that originate from parameters.
+ // See the OverridesController.Undecidable test cases for details.
+ var hasActionFiltersOverrides = false;
+ var candidates = new ConcurrentStack(); // In SymbolEnd, we filter the candidates based on the overriding we learn on the go.
+ if (symbolStart.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType())
+ {
+ symbolStart.RegisterCodeBlockStartAction(codeBlockStart =>
+ {
+ if (IsOverridingFilterMethods(codeBlockStart.OwningSymbol))
+ {
+ // We do not want to raise in ActionFilter overrides and so we do not register.
+ // The SymbolEndAction needs to be made aware, that there are
+ // ActionFilter overrides, so it can filter out some candidates.
+ hasActionFiltersOverrides = true;
+ }
+ else
+ {
+ RegisterCodeBlockActions(codeBlockStart, argumentDescriptors, propertyAccessDescriptors, candidates);
+ }
+ });
+ }
+ symbolStart.RegisterSymbolEndAction(symbolEnd =>
+ {
+ foreach (var candidate in candidates.Where(x => !(hasActionFiltersOverrides && x.OriginatesFromParameter)))
+ {
+ symbolEnd.ReportIssue(Diagnostic.Create(Rule, candidate.Location, candidate.Message));
+ }
+ });
+ }, SymbolKind.NamedType);
+ }
+ });
+ }
+
+ private void RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext codeBlockStart,
+ ArgumentDescriptor[] argumentDescriptors, MemberDescriptor[] propertyAccessDescriptors,
+ ConcurrentStack controllerCandidates)
+ {
+ // Within a single code block, access via constant and variable keys could be mixed.
+ // We only want to raise, if all access were done via constants.
+ var allConstantAccesses = true;
+ var codeBlockCandidates = new ConcurrentStack();
+ if (argumentDescriptors.Any())
+ {
+ codeBlockStart.RegisterNodeAction(nodeContext =>
+ {
+ var argument = (ArgumentSyntax)nodeContext.Node;
+ var context = new ArgumentContext(argument, nodeContext.SemanticModel);
+ if (allConstantAccesses && Array.Exists(argumentDescriptors, x => Language.Tracker.Argument.MatchArgument(x)(context)))
+ {
+ allConstantAccesses &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string };
+ codeBlockCandidates.Push(new(UseAspNetModelBindingMessage, GetPrimaryLocation(argument), IsOriginatingFromParameter(nodeContext.SemanticModel, argument)));
+ }
+ }, SyntaxKind.Argument);
+ }
+ if (propertyAccessDescriptors.Any())
+ {
+ codeBlockStart.RegisterNodeAction(nodeContext =>
+ {
+ // The property access of Request.Form.Files can be replaced by an IFormFile binding.
+ // Any access to a "Files" property is therefore noncompliant. This is different from the Argument handling above.
+ var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node;
+ var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText);
+ if (Language.Tracker.PropertyAccess.MatchProperty(propertyAccessDescriptors)(context))
+ {
+ codeBlockCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), IsOriginatingFromParameter(nodeContext.SemanticModel, memberAccess)));
+ }
+ }, SyntaxKind.SimpleMemberAccessExpression);
+ }
+ codeBlockStart.RegisterCodeBlockEndAction(codeBlockEnd =>
+ {
+ if (allConstantAccesses)
+ {
+ controllerCandidates.PushRange([.. codeBlockCandidates]);
+ }
+ });
+ }
+
+ private static (ArgumentDescriptor[] ArgumentDescriptors, MemberDescriptor[] PropertyAccessDescriptors) GetDescriptors(Compilation compilation)
+ {
+ var argumentDescriptors = new List();
+ var propertyAccessDescriptors = new List();
+ if (compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { })
+ {
+ AddAspNetCoreDescriptors(argumentDescriptors, propertyAccessDescriptors);
+ }
+ // TODO: Add descriptors for Asp.Net MVC 4.x
+ return ([.. argumentDescriptors], [.. propertyAccessDescriptors]);
+ }
+
+ private static void AddAspNetCoreDescriptors(List argumentDescriptors, List propertyAccessDescriptors)
+ {
+ argumentDescriptors.AddRange([
+ ArgumentDescriptor.ElementAccess(// Request.Form["id"]
+ invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection,
+ invokedIndexerExpression: "Form",
+ parameterConstraint: _ => true, // There is only a single overload and it is getter only
+ argumentPosition: 0),
+ ArgumentDescriptor.MethodInvocation(// Request.Form.TryGetValue("id", out _)
+ invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection,
+ methodName: "TryGetValue",
+ parameterName: "key",
+ argumentPosition: 0),
+ ArgumentDescriptor.MethodInvocation(// Request.Form.ContainsKey("id")
+ invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection,
+ methodName: "ContainsKey",
+ parameterName: "key",
+ argumentPosition: 0),
+ ArgumentDescriptor.ElementAccess(// Request.Headers["id"]
+ invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary,
+ invokedIndexerExpression: "Headers",
+ parameterConstraint: IsGetterParameter, // Headers are read/write
+ argumentPosition: 0),
+ ArgumentDescriptor.MethodInvocation(// Request.Headers.TryGetValue("id", out _)
+ invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), // TryGetValue is from IDictionary here. We check the type arguments.
+ invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison),
+ invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary,
+ parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal),
+ argumentListConstraint: (list, position) => list.Count == 2 && position is 0 or null,
+ refKind: RefKind.None),
+ ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id")
+ invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"),
+ invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "ContainsKey", comparison),
+ invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary,
+ parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal),
+ argumentListConstraint: (list, _) => list.Count == 1,
+ refKind: RefKind.None),
+ ArgumentDescriptor.ElementAccess(// Request.Query["id"]
+ invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IQueryCollection,
+ invokedIndexerExpression: "Query",
+ parameterConstraint: _ => true, // There is only a single overload and it is getter only
+ argumentPosition: 0),
+ ArgumentDescriptor.MethodInvocation(// Request.Query.TryGetValue("id", out _)
+ invokedType: KnownType.Microsoft_AspNetCore_Http_IQueryCollection,
+ methodName: "TryGetValue",
+ parameterName: "key",
+ argumentPosition: 0),
+ ArgumentDescriptor.ElementAccess(// Request.RouteValues["id"]
+ invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary,
+ invokedIndexerExpression: "RouteValues",
+ parameterConstraint: IsGetterParameter, // RouteValues are read/write
+ argumentPosition: 0),
+ ArgumentDescriptor.MethodInvocation(// Request.RouteValues.TryGetValue("id", out _)
+ invokedType: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary,
+ methodName: "TryGetValue",
+ parameterName: "key",
+ argumentPosition: 0)]);
+
+ propertyAccessDescriptors.Add(new(KnownType.Microsoft_AspNetCore_Http_IFormCollection, "Files")); // Request.Form.Files...
+ }
+
+ // Check that the "Headers" expression in the Headers.TryGetValue("id", out _) invocation is of type IHeaderDictionary
+ private static bool IsAccessedViaHeaderDictionary(SemanticModel model, ILanguageFacade language, SyntaxNode invocation) =>
+ invocation is InvocationExpressionSyntax { Expression: { } expression }
+ && GetLeftOfDot(expression) is { } left
+ && model.GetTypeInfo(left) is { Type: { } typeSymbol } && typeSymbol.Is(KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary);
+
+ private static bool IsOverridingFilterMethods(ISymbol owningSymbol) =>
+ (owningSymbol.GetOverriddenMember() ?? owningSymbol).ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container }
+ && container.IsAny(
+ KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter,
+ KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter));
+
+ private static bool IsOriginatingFromParameter(SemanticModel semanticModel, ArgumentSyntax argument) =>
+ GetExpressionOfArgumentParent(argument) is { } parentExpression && IsOriginatingFromParameter(semanticModel, parentExpression);
+
+ private static bool IsOriginatingFromParameter(SemanticModel semanticModel, ExpressionSyntax expression) =>
+ MostLeftOfDottedChain(expression) is { } mostLeft && semanticModel.GetSymbolInfo(mostLeft).Symbol is IParameterSymbol;
+
+ private static ExpressionSyntax GetLeftOfDot(ExpressionSyntax expression) =>
+ expression switch
+ {
+ MemberAccessExpressionSyntax memberAccessExpression => memberAccessExpression.Expression,
+ MemberBindingExpressionSyntax memberBindingExpression => memberBindingExpression.GetParentConditionalAccessExpression()?.Expression,
+ _ => null,
+ };
+
+ private static ExpressionSyntax MostLeftOfDottedChain(ExpressionSyntax root)
+ {
+ var current = root.GetRootConditionalAccessExpression()?.Expression ?? root;
+ while (current.Kind() is SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.ElementAccessExpression)
+ {
+ current = current switch
+ {
+ MemberAccessExpressionSyntax { Expression: { } left } => left,
+ ElementAccessExpressionSyntax { Expression: { } left } => left,
+ _ => throw new InvalidOperationException("Unreachable"),
+ };
+ }
+ return current;
+ }
+
+ private static ExpressionSyntax GetExpressionOfArgumentParent(ArgumentSyntax argument) =>
+ argument switch
+ {
+ { Parent: BracketedArgumentListSyntax { Parent: ElementBindingExpressionSyntax expression } } => expression.GetParentConditionalAccessExpression(),
+ { Parent: BracketedArgumentListSyntax { Parent: ElementAccessExpressionSyntax { Expression: { } expression } } } => expression,
+ { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax { Expression: { } expression } } } => expression,
+ _ => null,
+ };
+
+ private static Location GetPrimaryLocation(ArgumentSyntax argument) =>
+ ((SyntaxNode)GetExpressionOfArgumentParent(argument) ?? argument).GetLocation();
+
+ private static bool IsGetterParameter(IParameterSymbol parameter) =>
+ parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.PropertyGet };
+
+ private static bool IsIDictionaryStringStringValuesInvocation(IMethodSymbol method, string name) =>
+ method.Is(KnownType.System_Collections_Generic_IDictionary_TKey_TValue, name)
+ && method.ContainingType.TypeArguments is { Length: 2 } typeArguments
+ && typeArguments[0].Is(KnownType.System_String)
+ && typeArguments[1].Is(KnownType.Microsoft_Extensions_Primitives_StringValues);
+
+ private readonly record struct ReportCandidate(string Message, Location Location, bool OriginatesFromParameter);
+}
diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
index 4e0e9834b1a..05cd7ef8d69 100644
--- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
@@ -69,6 +69,7 @@ public sealed partial class KnownType
public static readonly KnownType Microsoft_AspNetCore_Http_IFormFile = new("Microsoft.AspNetCore.Http.IFormFile");
public static readonly KnownType Microsoft_AspNetCore_Http_IFormFileCollection = new("Microsoft.AspNetCore.Http.IFormFileCollection");
public static readonly KnownType Microsoft_AspNetCore_Http_IHeaderDictionary = new("Microsoft.AspNetCore.Http.IHeaderDictionary");
+ public static readonly KnownType Microsoft_AspNetCore_Http_IQueryCollection = new("Microsoft.AspNetCore.Http.IQueryCollection");
public static readonly KnownType Microsoft_AspNetCore_Http_IRequestCookieCollection = new("Microsoft.AspNetCore.Http.IRequestCookieCollection");
public static readonly KnownType Microsoft_AspNetCore_Http_IResponseCookies = new("Microsoft.AspNetCore.Http.IResponseCookies");
public static readonly KnownType Microsoft_AspNetCore_Http_IResult = new("Microsoft.AspNetCore.Http.IResult");
@@ -83,6 +84,8 @@ public sealed partial class KnownType
public static readonly KnownType Microsoft_AspNetCore_Mvc_ControllerAttribute = new("Microsoft.AspNetCore.Mvc.ControllerAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_DisableRequestSizeLimitAttribute = new("Microsoft.AspNetCore.Mvc.DisableRequestSizeLimitAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_Filters_ActionFilterAttribute = new("Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute");
+ public static readonly KnownType Microsoft_AspNetCore_Mvc_Filters_IActionFilter = new("Microsoft.AspNetCore.Mvc.Filters.IActionFilter");
+ public static readonly KnownType Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter = new("Microsoft.AspNetCore.Mvc.Filters.IAsyncActionFilter");
public static readonly KnownType Microsoft_AspNetCore_Mvc_FromServicesAttribute = new("Microsoft.AspNetCore.Mvc.FromServicesAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_HttpDeleteAttribute = new("Microsoft.AspNetCore.Mvc.HttpDeleteAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_HttpGetAttribute = new("Microsoft.AspNetCore.Mvc.HttpGetAttribute");
@@ -109,6 +112,7 @@ public sealed partial class KnownType
public static readonly KnownType Microsoft_AspNetCore_Mvc_RouteAttribute = new("Microsoft.AspNetCore.Mvc.RouteAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_Routing_HttpMethodAttribute = new("Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute");
public static readonly KnownType Microsoft_AspNetCore_Razor_Hosting_RazorCompiledItemAttribute = new("Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute");
+ public static readonly KnownType Microsoft_AspNetCore_Routing_RouteValueDictionary = new("Microsoft.AspNetCore.Routing.RouteValueDictionary");
public static readonly KnownType Microsoft_Azure_Cosmos_CosmosClient = new("Microsoft.Azure.Cosmos.CosmosClient");
public static readonly KnownType Microsoft_Azure_Documents_Client_DocumentClient = new("Microsoft.Azure.Documents.Client.DocumentClient");
public static readonly KnownType Microsoft_Azure_ServiceBus_Management_ManagementClient = new("Microsoft.Azure.ServiceBus.Management.ManagementClient");
diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs
index a31699a9d90..c331e3d7f1d 100644
--- a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs
@@ -30,5 +30,11 @@ public PropertyAccessContext(SonarSyntaxNodeReportingContext context, string pro
PropertyName = propertyName;
PropertySymbol = new Lazy(() => context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IPropertySymbol);
}
+
+ public PropertyAccessContext(SyntaxNode node, SemanticModel semanticModel, string propertyName) : base(node, semanticModel)
+ {
+ PropertyName = propertyName;
+ PropertySymbol = new Lazy(() => semanticModel.GetSymbolInfo(node).Symbol as IPropertySymbol);
+ }
}
}
diff --git a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
index ba7bd4005c3..b12a0f5fec9 100644
--- a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
+++ b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
@@ -6856,8 +6856,7 @@ internal static class RuleTypeMappingCS
// ["S6929"],
["S6930"] = "BUG",
["S6931"] = "CODE_SMELL",
- // ["S6932"],
- // ["S6933"],
+ ["S6932"] = "CODE_SMELL",
["S6934"] = "CODE_SMELL",
// ["S6935"],
// ["S6936"],
diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs
new file mode 100644
index 00000000000..9ca56824c70
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs
@@ -0,0 +1,296 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2024 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SonarAnalyzer.Rules.CSharp;
+
+namespace SonarAnalyzer.Test.Rules;
+
+[TestClass]
+public class UseAspNetModelBindingTest
+{
+#if NET
+ private readonly VerifierBuilder builderAspNetCore = new VerifierBuilder()
+ .WithOptions(ParseOptionsHelper.FromCSharp12)
+ .AddReferences([
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcCore,
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpAbstractions,
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcViewFeatures,
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcAbstractions,
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpFeatures,
+ AspNetCoreMetadataReference.MicrosoftExtensionsPrimitives,
+ ]);
+
+ [TestMethod]
+ public void UseAspNetModelBinding_NoRegistrationIfNotAspNet() =>
+ new VerifierBuilder().AddSnippet(string.Empty).Verify();
+
+ [TestMethod]
+ public void UseAspNetModelBinding_AspNetCore_CSharp12() =>
+ builderAspNetCore.AddPaths("UseAspNetModelBinding_AspNetCore.cs").Verify();
+
+ [DataTestMethod]
+ [DataRow("Form")]
+ [DataRow("Query")]
+ [DataRow("RouteValues")]
+ [DataRow("Headers")]
+ public void UseAspNetModelBinding_NonCompliantAccess(string property) =>
+ builderAspNetCore.AddSnippet($$""""
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.AspNetCore.Mvc.Filters;
+ using System;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ public class TestController : Controller
+ {
+ async Task NoncompliantKeyVariations()
+ {
+ _ = Request.{{property}}[@"key"]; // Noncompliant
+ _ = Request.{{property}}.TryGetValue(@"key", out _); // Noncompliant
+ _ = Request.{{property}}["""key"""]; // Noncompliant
+ _ = Request.{{property}}.TryGetValue("""key""", out _); // Noncompliant
+
+ const string key = "id";
+ _ = Request.{{property}}[key]; // Noncompliant
+ _ = Request.{{property}}.TryGetValue(key, out _); // Noncompliant
+ _ = Request.{{property}}[$"prefix.{key}"]; // Noncompliant
+ _ = Request.{{property}}.TryGetValue($"prefix.{key}", out _); // Noncompliant
+ _ = Request.{{property}}[$"""prefix.{key}"""]; // Noncompliant
+ _ = Request.{{property}}.TryGetValue($"""prefix.{key}""", out _); // Noncompliant
+
+ _ = Request.{{property}}[key: "id"]; // Noncompliant
+ _ = Request.{{property}}.TryGetValue(value: out _, key: "id"); // Noncompliant
+ }
+
+ private static void HandleRequest(HttpRequest request)
+ {
+ _ = request.{{property}}["id"]; // Noncompliant: Containing type is a controller
+ void LocalFunction()
+ {
+ _ = request.{{property}}["id"]; // Noncompliant: Containing type is a controller
+ }
+ static void StaticLocalFunction(HttpRequest request)
+ {
+ _ = request.{{property}}["id"]; // Noncompliant: Containing type is a controller
+ }
+ }
+ }
+ """").Verify();
+ /*
+ * Depends on https://github.com/SonarSource/sonar-dotnet/pull/8949
+ [TestMethod]
+ [CombinatorialData]
+ public void UseAspNetModelBinding_CompliantAccess(
+ [DataValues(
+ "_ = {0}.Keys",
+ "_ = {0}.Count",
+ "foreach (var kvp in {0}) {{ }}",
+ "_ = {0}.Select(x => x);",
+ "_ = {0}[key]; // Compliant: The accessed key is not a compile time constant")] string statementFormat,
+ [DataValues("Request", "this.Request", "ControllerContext.HttpContext.Request", "request")] string request,
+ [DataValues("Form", "Headers", "Query", "RouteValues")] string property,
+ [DataValues("[FromForm]", "[FromQuery]", "[FromRoute]", "[FromHeader]")] string attribute) =>
+ builderAspNetCore.AddSnippet($$"""
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.AspNetCore.Mvc.Filters;
+ using System;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ public class TestController : Controller
+ {
+ async Task Compliant({{attribute}} string key, HttpRequest request)
+ {
+ {{string.Format(statementFormat, $"{request}.{property}")}};
+ }
+ }
+ """).Verify();
+ */
+
+ [DataTestMethod]
+ [DataRow("Form")]
+ [DataRow("Headers")]
+ [DataRow("Query")]
+ [DataRow("RouteValues")]
+ public void UseAspNetModelBinding_DottedExpressions(string property) =>
+ builderAspNetCore.AddSnippet($$"""
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.AspNetCore.Mvc.Filters;
+ using Microsoft.AspNetCore.Routing;
+ using System;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ public class TestController : Controller
+ {
+ HttpRequest ValidRequest => Request;
+ IFormCollection Form => Request.Form;
+ IHeaderDictionary Headers => Request.Headers;
+ IQueryCollection Query => Request.Query;
+ RouteValueDictionary RouteValues => Request.RouteValues;
+
+ async Task DottedExpressions()
+ {
+ _ = (true ? Request : Request).{{property}}["id"]; // Noncompliant
+ _ = ValidatedRequest().{{property}}["id"]; // Noncompliant
+ _ = ValidRequest.{{property}}["id"]; // Noncompliant
+ _ = {{property}}["id"]; // Noncompliant
+ _ = this.{{property}}["id"]; // Noncompliant
+ _ = new TestController().{{property}}["id"]; // Noncompliant
+
+ _ = this.Request.{{property}}["id"]; // Noncompliant
+ _ = Request?.{{property}}?["id"]; // Noncompliant
+ _ = Request?.{{property}}?.TryGetValue("id", out _); // Noncompliant
+ _ = Request.{{property}}?.TryGetValue("id", out _); // Noncompliant
+ _ = Request.{{property}}?.TryGetValue("id", out _).ToString(); // Noncompliant
+ _ = HttpContext.Request.{{property}}["id"]; // Noncompliant
+ _ = Request.HttpContext.Request.{{property}}["id"]; // Noncompliant
+ _ = this.ControllerContext.HttpContext.Request.{{property}}["id"]; // Noncompliant
+ var r1 = HttpContext.Request;
+ _ = r1.{{property}}["id"]; // Noncompliant
+ var r2 = ControllerContext;
+ _ = r2.HttpContext.Request.{{property}}["id"]; // Noncompliant
+
+ HttpRequest ValidatedRequest() => Request;
+ }
+ }
+ """).Verify();
+
+ [DataTestMethod]
+ [DataRow("public class MyController: Controller")]
+ [DataRow("public class MyController: ControllerBase")]
+ [DataRow("[Controller] public class My: Controller")]
+ // [DataRow("public class MyController")] FN: Poco controller are not detected
+ public void UseAspNetModelBinding_PocoController(string classDeclaration) =>
+ builderAspNetCore.AddSnippet($$""""
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.AspNetCore.Mvc.Filters;
+ using System;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ {{classDeclaration}}
+ {
+ public async Task Action([FromServices]IHttpContextAccessor httpContextAccessor)
+ {
+ _ = httpContextAccessor.HttpContext.Request.Form["id"]; // Noncompliant
+ }
+ }
+ """").Verify();
+
+ [DataTestMethod]
+ [DataRow("public class My")]
+ [DataRow("[NonController] public class My: Controller")]
+ [DataRow("[NonController] public class MyController: Controller")]
+ public void UseAspNetModelBinding_NoController(string classDeclaration) =>
+ builderAspNetCore.AddSnippet($$""""
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.AspNetCore.Mvc.Filters;
+ using System;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ {{classDeclaration}}
+ {
+ public async Task Action([FromServices]IHttpContextAccessor httpContextAccessor)
+ {
+ _ = httpContextAccessor.HttpContext.Request.Form["id"]; // Compliant
+ }
+ }
+ """").Verify();
+
+ [DataTestMethod]
+ [DataRow("Form")]
+ [DataRow("Headers")]
+ [DataRow("Query")]
+ [DataRow("RouteValues")]
+ public void UseAspNetModelBinding_NoControllerHelpers(string property) =>
+ builderAspNetCore.AddSnippet($$""""
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.AspNetCore.Mvc.Filters;
+ using System;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ static class HttpRequestExtensions
+ {
+ public static void Ext(this HttpRequest request)
+ {
+ _ = request.{{property}}["id"]; // Compliant: Not in a controller
+ }
+ }
+
+ class RequestService
+ {
+ public HttpRequest Request { get; }
+
+ public void HandleRequest(HttpRequest request)
+ {
+ _ = Request.{{property}}["id"]; // Compliant: Not in a controller
+ _ = request.{{property}}["id"]; // Compliant: Not in a controller
+ }
+ }
+ """").Verify();
+
+ /*
+ * Depends on https://github.com/SonarSource/sonar-dotnet/pull/8949
+ [TestMethod]
+ [CombinatorialData]
+ public void UseAspNetModelBinding_InheritanceAccess(
+ [DataValues(
+ ": Controller",
+ ": ControllerBase",
+ ": MyBaseController",
+ ": MyBaseBaseController")]string baseList,
+ [DataValues(
+ """_ = Request.Form["id"]""",
+ """_ = Request.Form.TryGetValue("id", out var _)""",
+ """_ = Request.Headers["id"]""",
+ """_ = Request.Query["id"]""",
+ """_ = Request.RouteValues["id"]""")]string nonCompliantStatement) =>
+ builderAspNetCore.AddSnippet($$""""
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.AspNetCore.Mvc.Filters;
+ using System;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ public class MyBaseController : ControllerBase { }
+ public class MyBaseBaseController : MyBaseController { }
+
+ public class MyTestController {{baseList}}
+ {
+ public void Action()
+ {
+ {{nonCompliantStatement}}; // Noncompliant
+ }
+ }
+ """").Verify();
+ */
+#endif
+}
diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs
new file mode 100644
index 00000000000..5e47dd444f9
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs
@@ -0,0 +1,214 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+public class TestController : Controller
+{
+ private readonly string Key = "id";
+
+ public IActionResult Post(string key)
+ {
+ _ = Request.Form["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^
+ _ = Request.Form.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.ContainsKey("id"); // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^^
+ _ = Request.Headers["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^^^^
+ _ = Request.Headers.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ _ = Request.Headers.ContainsKey("id"); // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ _ = Request.Query["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^^
+ _ = Request.Query.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^
+ _ = Request.RouteValues["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^^^^^^^^
+ _ = Request.RouteValues.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.Files; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
+ // ^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.File\u0073; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.Files["file"]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
+ // ^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.Files[key]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
+ // ^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.Files[0]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
+ // ^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.Files.Any(); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
+ // ^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.Files.Count; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
+ // ^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.Files.GetFile("file"); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
+ // ^^^^^^^^^^^^^^^^^^
+ _ = Request.Form.Files.GetFiles("file"); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
+ // ^^^^^^^^^^^^^^^^^^
+ return default;
+ }
+
+ void MixedAccess_Form(string key)
+ {
+ _ = Request.Form["id"]; // Compliant (a mixed access with constant and non-constant keys is compliant)
+ _ = Request.Form[key]; // Compliant
+ }
+
+ void MixedAccess_Form_Query(string key)
+ {
+ _ = Request.Form["id"]; // Compliant (a mixed access with constant and non-constant keys is compliant)
+ _ = Request.Query[key]; // Compliant
+ }
+
+ void FalseNegatives()
+ {
+ string localKey = "id";
+ _ = Request.Form[localKey]; // FN (Requires SE)
+ _ = Request.Form[Key]; // FN: Key is a readonly field with a constant initializer (Requires cross procedure SE)
+ }
+
+ void FormCollection(IFormCollection form)
+ {
+ _ = form["id"]; // Compliant. Using IFormCollection is model binding
+ }
+
+ void WriteAccess()
+ {
+ Request.Headers["id"] = "Assignment"; // Compliant
+ Request.RouteValues["id"] = "Assignment"; // Compliant
+ }
+
+ async Task Compliant()
+ {
+ _ = Request.Cookies["cookie"]; // Compliant: Cookies are not bound by default
+ _ = Request.QueryString; // Compliant: Accessing the whole raw string is fine.
+ }
+
+ async Task CompliantFormAccess()
+ {
+ var form = await Request.ReadFormAsync(); // Compliant: This might be used for optimization purposes e.g. conditional form value access.
+ _ = form["id"];
+ }
+}
+
+public class CodeBlocksController : Controller
+{
+ public CodeBlocksController()
+ {
+ _ = Request.Form["id"]; // Noncompliant
+ }
+
+ public CodeBlocksController(object o) => _ = Request.Form["id"]; // Noncompliant
+
+ HttpRequest ValidRequest => Request;
+ IFormCollection Form => Request.Form;
+
+ string P1 => Request.Form["id"]; // Noncompliant
+ string P2
+ {
+ get => Request.Form["id"]; // Noncompliant
+ }
+ string P3
+ {
+ get
+ {
+ return Request.Form["id"]; // Noncompliant
+ }
+ }
+ void M1() => _ = Request.Form["id"]; // Noncompliant
+ void M2()
+ {
+ Func f1 = () => Request.Form["id"]; // Noncompliant
+ Func f2 = x => Request.Form["id"]; // Noncompliant
+ Func f3 = delegate (object x) { return Request.Form["id"]; }; // Noncompliant
+ }
+ void M3()
+ {
+ // see also parameterized test "DottedExpressions"
+ _ = Request.Form["id"][0]; // Noncompliant
+ _ = Request?.Form["id"][0]; // Noncompliant
+ _ = Request.Form?["id"][0]; // Noncompliant
+ _ = Request?.Form?["id"][0]; // Noncompliant
+ _ = Request.Form?["id"][0]; // Noncompliant
+
+ _ = Request.Form?["id"][0][0]; // Noncompliant
+ _ = Request.Form?["id"][0]?[0]; // Noncompliant
+ _ = Request.Form["id"][0]?[0]; // Noncompliant
+ }
+ ~CodeBlocksController() => _ = Request.Form["id"]; // Noncompliant
+}
+
+public class OverridesController : Controller
+{
+ public void Action()
+ {
+ _ = Request.Form["id"]; // Noncompliant
+ }
+ private void Undecidable(HttpContext context)
+ {
+ // Implementation: It might be difficult to distinguish between access to "Request" that originate from overrides vs. "Request" access that originate from action methods.
+ // This is especially true for "Request" which originate from parameters like here. We may need to redeclare such cases as FNs (see e.g HandleRequest above).
+ _ = context.Request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overrides (which don't).
+ _ = context.Request.Form["id"][0];
+ _ = context.Request?.Form["id"][0];
+ _ = context.Request.Form?["id"][0];
+ _ = context.Request?.Form?["id"][0];
+ _ = context.Request.Form?["id"][0];
+
+ _ = context.Request.Form?["id"][0][0];
+ _ = context.Request.Form?["id"][0]?[0];
+ _ = context.Request.Form["id"][0]?[0];
+ }
+ private void Undecidable(HttpRequest request)
+ {
+ _ = request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overloads (which don't).
+ }
+ public override void OnActionExecuted(ActionExecutedContext context)
+ {
+ _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here
+ }
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here
+ }
+ public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here
+ return base.OnActionExecutionAsync(context, next);
+ }
+}
+
+[Controller]
+public class PocoController : IActionFilter, IAsyncActionFilter
+{
+ public void OnActionExecuted(ActionExecutedContext context)
+ {
+ _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here
+ }
+ void IActionFilter.OnActionExecuted(Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext context)
+ {
+ _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here
+ }
+ public void OnActionExecuting(ActionExecutingContext context)
+ {
+ _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here
+ }
+ void IActionFilter.OnActionExecuting(ActionExecutingContext context)
+ {
+ _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here
+ }
+ public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here
+ return Task.CompletedTask;
+ }
+ Task IAsyncActionFilter.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here
+ return Task.CompletedTask;
+ }
+}