From 800349dd11065ed6e5edaafef82d0ea2997284a3 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 26 Aug 2026 15:37:43 +0200 Subject: [PATCH 1/5] C#: Add global ASP.NET Core - enable validation for all action methods. --- .../MissingAntiForgeryTokenValidation.cs | 39 +++++++++++++++++++ ...MissingAntiForgeryTokenValidation.expected | 1 + .../MissingAntiForgeryTokenValidation.qlref | 1 + .../CWE-352/global-aspnetcore/options | 2 + 4 files changed, 43 insertions(+) create mode 100644 csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.cs create mode 100644 csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.expected create mode 100644 csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.qlref create mode 100644 csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/options diff --git a/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.cs b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.cs new file mode 100644 index 000000000000..438ad03f3200 --- /dev/null +++ b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +public class HomeController : Controller +{ + // GOOD: This is validated by the global filter. + [HttpPost] + public ActionResult Login() + { + return View(); + } + + // GOOD: Antiforgery token is validated explicitly. + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult UpdateDetails() + { + return View(); + } +} + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Register MVC controllers and Razor views. + // The global filter automatically validates antiforgery tokens + // for unsafe HTTP methods such as POST, PUT, PATCH, and DELETE. + builder.Services.AddControllersWithViews(options => + { + options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); + }); + } +} diff --git a/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.expected b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.expected new file mode 100644 index 000000000000..459ba06f47d0 --- /dev/null +++ b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.expected @@ -0,0 +1 @@ +| MissingAntiForgeryTokenValidation.cs:11:25:11:29 | Login | Method 'Login' handles a POST request without performing CSRF token validation. | diff --git a/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.qlref b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.qlref new file mode 100644 index 000000000000..5e1ab2426c65 --- /dev/null +++ b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.qlref @@ -0,0 +1 @@ +query: Security Features/CWE-352/MissingAntiForgeryTokenValidation.ql diff --git a/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/options b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/options new file mode 100644 index 000000000000..698ad488b6d4 --- /dev/null +++ b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/options @@ -0,0 +1,2 @@ +semmle-extractor-options: /nostdlib /noconfig +semmle-extractor-options: --load-sources-from-project:${testdir}/../../../../resources/stubs/_frameworks/Microsoft.AspNetCore.App/Microsoft.AspNetCore.App.csproj From f0b45a0519c7c6191b77e767ebf86a4b5f2e486a Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 26 Aug 2026 15:40:23 +0200 Subject: [PATCH 2/5] C#: Detect possible global auto validate configuration to remove FPs. --- .../frameworks/microsoft/AspNetCore.qll | 28 +++++++++++++++---- .../MissingAntiForgeryTokenValidation.ql | 14 ++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/csharp/ql/lib/semmle/code/csharp/frameworks/microsoft/AspNetCore.qll b/csharp/ql/lib/semmle/code/csharp/frameworks/microsoft/AspNetCore.qll index abdd81646828..c71e30d6a1b9 100644 --- a/csharp/ql/lib/semmle/code/csharp/frameworks/microsoft/AspNetCore.qll +++ b/csharp/ql/lib/semmle/code/csharp/frameworks/microsoft/AspNetCore.qll @@ -144,6 +144,15 @@ class ValidateAntiForgeryAttribute extends Attribute { } } +/** + * The `Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute` class. + */ +class AutoValidateAntiforgeryTokenAttribute extends Class { + AutoValidateAntiforgeryTokenAttribute() { + this.hasFullyQualifiedName("Microsoft.AspNetCore.Mvc", "AutoValidateAntiforgeryTokenAttribute") + } +} + /** * A class that has a name like `[Auto...]Validate[...]Anti[Ff]orgery[...Token]` and implements `IFilterMetadata` interface * This class can be added to a collection of global `MvcOptions.Filters` collection. @@ -230,11 +239,20 @@ private Assembly getAnAssemblyFor(Type type) { result = getACompilationFor(type).getOutputAssembly() } -private predicate isMicrosoftAspNetCoreMvcRegistration(MethodCall call) { - call.getTarget() - .hasFullyQualifiedName("Microsoft.Extensions.DependencyInjection", - ["MvcServiceCollectionExtensions", "MvcCoreServiceCollectionExtensions"], - ["AddControllers", "AddControllersWithViews", "AddMvc", "AddMvcCore"]) +/** + * A method that is a registration of an ASP.NET Core MVC service, i.e. `AddControllers`, `AddControllersWithViews`, `AddMvc`, or `AddMvcCore`. + */ +class MicrosoftAspNetCoreMvcRegistration extends Method { + MicrosoftAspNetCoreMvcRegistration() { + this.hasFullyQualifiedName("Microsoft.Extensions.DependencyInjection", + ["MvcServiceCollectionExtensions", "MvcCoreServiceCollectionExtensions"], + ["AddControllers", "AddControllersWithViews", "AddMvc", "AddMvcCore"]) + } +} + +/** Holds if the method call is a registration of an ASP.NET Core MVC service. */ +predicate isMicrosoftAspNetCoreMvcRegistration(MethodCall call) { + call.getTarget() instanceof MicrosoftAspNetCoreMvcRegistration } private predicate isMicrosoftAspNetCoreMvcApplication(Compilation compilation) { diff --git a/csharp/ql/src/Security Features/CWE-352/MissingAntiForgeryTokenValidation.ql b/csharp/ql/src/Security Features/CWE-352/MissingAntiForgeryTokenValidation.ql index 77a3f2b59450..677bba60015c 100644 --- a/csharp/ql/src/Security Features/CWE-352/MissingAntiForgeryTokenValidation.ql +++ b/csharp/ql/src/Security Features/CWE-352/MissingAntiForgeryTokenValidation.ql @@ -36,8 +36,6 @@ private Method getAStartedMethod() { /** * Holds if the project has a global anti forgery filter. - * - * No AspNetCore case here as the corresponding class doesn't seem to exist. */ predicate hasGlobalAntiForgeryFilter() { // A global filter added @@ -49,6 +47,18 @@ predicate hasGlobalAntiForgeryFilter() { // The filter is added by the Application_Start() method getAStartedMethod() = addGlobalFilter.getEnclosingCallable() ) + or + exists(MethodCall addGlobalFilter, MethodCall registrationCall | + addGlobalFilter.getTarget() = + any(AspNetCore::MicrosoftAspNetCoreMvcFilterCollection collection).getAddMethod() and + // The filter is the `AutoValidateAntiforgeryTokenAttribute` filter. + addGlobalFilter.getArgument(0).getType() instanceof + AspNetCore::AutoValidateAntiforgeryTokenAttribute and + // The filter is added in an ASP.NET Core registration call, which is provided as a lambda argument + // to the Mvc registration method. + registrationCall.getTarget() instanceof AspNetCore::MicrosoftAspNetCoreMvcRegistration and + registrationCall.getAnArgument() = addGlobalFilter.getEnclosingCallable() + ) } private class RequireAntiforgeryTokenAttribute extends Attribute { From 56388fa1e3aaecc34a504739f36389b1dd5e0468 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 26 Aug 2026 16:00:20 +0200 Subject: [PATCH 3/5] C#: Update test expected output. --- .../global-aspnetcore/MissingAntiForgeryTokenValidation.expected | 1 - 1 file changed, 1 deletion(-) diff --git a/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.expected b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.expected index 459ba06f47d0..e69de29bb2d1 100644 --- a/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.expected +++ b/csharp/ql/test/query-tests/Security Features/CWE-352/global-aspnetcore/MissingAntiForgeryTokenValidation.expected @@ -1 +0,0 @@ -| MissingAntiForgeryTokenValidation.cs:11:25:11:29 | Login | Method 'Login' handles a POST request without performing CSRF token validation. | From 9177ccec6182200e8f7a9eb71d8ef366fd1b999e Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 28 Aug 2026 09:21:43 +0200 Subject: [PATCH 4/5] C#: Add change-note. --- csharp/ql/src/change-notes/2026-08-27-csrf-autovalidate.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 csharp/ql/src/change-notes/2026-08-27-csrf-autovalidate.md diff --git a/csharp/ql/src/change-notes/2026-08-27-csrf-autovalidate.md b/csharp/ql/src/change-notes/2026-08-27-csrf-autovalidate.md new file mode 100644 index 000000000000..7ba5dbe4467c --- /dev/null +++ b/csharp/ql/src/change-notes/2026-08-27-csrf-autovalidate.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The `cs/web/missing-token-validation` query now recognizes an ASP.NET Core `AutoValidateAntiforgeryTokenAttribute` registered as a global MVC filter through `AddControllersWithViews` (and friends), avoiding false-positive results for covered actions. From 2107cd255683ca2364abd5fa95f0c7aa33aae288 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 28 Aug 2026 10:43:51 +0200 Subject: [PATCH 5/5] C#: Make the global validation filter compilation aware. --- .../MissingAntiForgeryTokenValidation.ql | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/csharp/ql/src/Security Features/CWE-352/MissingAntiForgeryTokenValidation.ql b/csharp/ql/src/Security Features/CWE-352/MissingAntiForgeryTokenValidation.ql index 677bba60015c..89a53cfa8d4e 100644 --- a/csharp/ql/src/Security Features/CWE-352/MissingAntiForgeryTokenValidation.ql +++ b/csharp/ql/src/Security Features/CWE-352/MissingAntiForgeryTokenValidation.ql @@ -12,6 +12,7 @@ */ import csharp +import semmle.code.csharp.commons.Compilation import semmle.code.csharp.frameworks.system.Web import semmle.code.csharp.frameworks.system.web.Helpers import semmle.code.csharp.frameworks.system.web.Mvc @@ -34,20 +35,19 @@ private Method getAStartedMethod() { getAStartedMethod().calls(result) } -/** - * Holds if the project has a global anti forgery filter. - */ -predicate hasGlobalAntiForgeryFilter() { - // A global filter added +private predicate hasGlobalWebMvcAntiforgeryFilter(Compilation compilation) { exists(MethodCall addGlobalFilter | // addGlobalFilter adds a filter to the global filter collection addGlobalFilter.getTarget() = any(GlobalFilterCollection gfc).getAddMethod() and // The filter is an antiforgery filter addGlobalFilter.getArgumentForName("filter").getType() instanceof AntiForgeryAuthorizationFilter and // The filter is added by the Application_Start() method - getAStartedMethod() = addGlobalFilter.getEnclosingCallable() + getAStartedMethod() = addGlobalFilter.getEnclosingCallable() and + addGlobalFilter.getTarget().getFile() = compilation.getAFileCompiled() ) - or +} + +predicate hasGlobalAspNetMvcAntiForgeryFilter(Compilation compilation) { exists(MethodCall addGlobalFilter, MethodCall registrationCall | addGlobalFilter.getTarget() = any(AspNetCore::MicrosoftAspNetCoreMvcFilterCollection collection).getAddMethod() and @@ -57,7 +57,8 @@ predicate hasGlobalAntiForgeryFilter() { // The filter is added in an ASP.NET Core registration call, which is provided as a lambda argument // to the Mvc registration method. registrationCall.getTarget() instanceof AspNetCore::MicrosoftAspNetCoreMvcRegistration and - registrationCall.getAnArgument() = addGlobalFilter.getEnclosingCallable() + registrationCall.getAnArgument() = addGlobalFilter.getEnclosingCallable() and + addGlobalFilter.getTarget().getFile() = compilation.getAFileCompiled() ) } @@ -77,11 +78,12 @@ private class RequireAntiforgeryTokenAttribute extends Attribute { } } -private predicate hasAspNetCoreAntiForgeryMiddleware() { +private predicate hasAspNetCoreAntiForgeryMiddleware(Compilation compilation) { exists(MethodCall call | call.getTarget() .hasFullyQualifiedName("Microsoft.AspNetCore.Builder", - "AntiforgeryApplicationBuilderExtensions", "UseAntiforgery") + "AntiforgeryApplicationBuilderExtensions", "UseAntiforgery") and + call.getFile() = compilation.getAFileCompiled() ) } @@ -116,7 +118,12 @@ private RequireAntiforgeryTokenAttribute getEffectiveRequireAntiforgeryTokenAttr class MvcControllerPostMethod extends Method { private Controller controller; - MvcControllerPostMethod() { controller.getAPostActionMethod() = this } + MvcControllerPostMethod() { + controller.getAPostActionMethod() = this and + exists(Compilation compilation | compilation.getAFileCompiled() = this.getFile() | + not hasGlobalWebMvcAntiforgeryFilter(compilation) + ) + } predicate hasValidateAntiForgeryAttribute() { this.getAnAttribute() instanceof ValidateAntiForgeryTokenAttribute or @@ -126,10 +133,13 @@ class MvcControllerPostMethod extends Method { class AspNetCoreControllerPostMethod extends Method { private AspNetCore::MicrosoftAspNetCoreMvcController controller; + private Compilation compilation; AspNetCoreControllerPostMethod() { controller.getAnActionMethod() = this and - this.getAnAttribute() instanceof AspNetCore::MicrosoftAspNetCoreMvcHttpPostAttribute + this.getAnAttribute() instanceof AspNetCore::MicrosoftAspNetCoreMvcHttpPostAttribute and + compilation.getAFileCompiled() = this.getFile() and + not hasGlobalAspNetMvcAntiForgeryFilter(compilation) } predicate hasValidateAntiForgeryAttribute() { @@ -138,7 +148,7 @@ class AspNetCoreControllerPostMethod extends Method { } predicate hasRequireAntiForgeryAttribute() { - hasAspNetCoreAntiForgeryMiddleware() and + hasAspNetCoreAntiForgeryMiddleware(compilation) and ( getEffectiveRequireAntiforgeryTokenAttributeOnMethod(this).requiresValidation() or @@ -167,7 +177,7 @@ Element getAValidatedElement() { or any(AspNetCore::ValidateAntiForgeryAttribute a).getTarget() = result or - hasAspNetCoreAntiForgeryMiddleware() and + hasAspNetCoreAntiForgeryMiddleware(_) and any(RequireAntiforgeryTokenAttribute a | a.requiresValidation()).getTarget() = result } @@ -177,9 +187,7 @@ where // Verify that validate anti forgery token attributes are used somewhere within this project, to // avoid reporting false positives on projects that use an alternative approach to mitigate CSRF // issues. - exists(getAValidatedElement()) and - // Also ignore cases where a global anti forgery filter is in use. - not hasGlobalAntiForgeryFilter() + exists(getAValidatedElement()) select postMethod, "Method '" + postMethod.getName() + "' handles a POST request without performing CSRF token validation."