-
Notifications
You must be signed in to change notification settings - Fork 238
New rule S6932: Use model binding instead of reading raw request data #8953
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
2a620cd
Copy rule implementation
martin-strecker-sonarsource e425389
Fix build error after re-base
martin-strecker-sonarsource 06324cc
Move UseAspNetModelBinding.cs to AspNet directory
martin-strecker-sonarsource 6013028
Move UseAspNetModelBindingTest.cs
martin-strecker-sonarsource 7a0e34d
Move UseAspNetModelBinding_AspNetCore.cs
martin-strecker-sonarsource 5fa675f
Fix verification and base path
martin-strecker-sonarsource c879138
Add array length check
martin-strecker-sonarsource 5a6a1f4
Re-factor
martin-strecker-sonarsource f3c6f7f
Move #if NET
martin-strecker-sonarsource f3cb1b1
Only write to allConstantAccesses if needed.
martin-strecker-sonarsource e4fa96c
Extract ArgumentMatching
martin-strecker-sonarsource 3b36922
Enable CombinatorialData tests
martin-strecker-sonarsource 751811b
Add null supression operator test cases
martin-strecker-sonarsource 1275364
Add S6933 back to the mappings
martin-strecker-sonarsource 694fcf6
Simplify cast
martin-strecker-sonarsource 96acacc
Move ActionFilters to static array
martin-strecker-sonarsource b12271d
Rename
martin-strecker-sonarsource 8557df4
Use "Latest" according to the new test guidelines
martin-strecker-sonarsource 62bb0b0
Formatting
martin-strecker-sonarsource File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
284 changes: 284 additions & 0 deletions
284
analyzers/src/SonarAnalyzer.CSharp/Rules/AspNet/UseAspNetModelBinding.cs
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
292 changes: 292 additions & 0 deletions
292
analyzers/tests/SonarAnalyzer.Test/Rules/AspNet/UseAspNetModelBindingTest.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,292 @@ | ||
| /* | ||
| * 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 SonarAnalyzer.Rules.CSharp; | ||
|
|
||
| namespace SonarAnalyzer.Test.Rules; | ||
|
|
||
| #if NET | ||
|
|
||
| [TestClass] | ||
| public class UseAspNetModelBindingTest | ||
| { | ||
| private readonly VerifierBuilder builderAspNetCore = new VerifierBuilder<UseAspNetModelBinding>() | ||
| .WithBasePath("AspNet") | ||
| .WithOptions(ParseOptionsHelper.FromCSharp12) | ||
zsolt-kolbay-sonarsource marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .AddReferences([ | ||
| AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcCore, | ||
| AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpAbstractions, | ||
| AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcViewFeatures, | ||
| AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcAbstractions, | ||
| AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpFeatures, | ||
| AspNetCoreMetadataReference.MicrosoftExtensionsPrimitives, | ||
| ]); | ||
|
|
||
| [TestMethod] | ||
| public void UseAspNetModelBinding_NoRegistrationIfNotAspNet() => | ||
| new VerifierBuilder<UseAspNetModelBinding>().AddSnippet(string.Empty).VerifyNoIssues(); | ||
|
|
||
| [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(); | ||
|
|
||
| [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}")}}; | ||
| } | ||
| } | ||
| """).VerifyNoIssues(); | ||
|
|
||
| [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 | ||
| } | ||
| } | ||
| """").VerifyNoIssues(); | ||
|
|
||
| [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 | ||
| } | ||
| } | ||
| """").VerifyNoIssues(); | ||
|
|
||
| [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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.