Review state-changing MVC actions without anti-forgery validation
Matches view-based MVC controllers (deriving from Microsoft.AspNetCore.Mvc.Controller or System.Web.Mvc.Controller) whose POST/PUT/DELETE/PATCH actions are not covered by [ValidateAntiForgeryToken] or [AutoValidateAntiforgeryToken] on the action, the class chain, or a global filter, and are not explicitly exempted with [IgnoreAntiforgeryToken]. Controllers or actions tagged [ApiController] or [Consumes(...)] are skipped: they are not reachable from an HTML form. Cookie-authenticated form posts without a token are the textbook CSRF (CWE-352).
Flagged Code
Flagged Code
public class AccountController : Controller
{
[HttpPost] // flagged: no anti-forgery validation
public IActionResult UpdateEmail(string email)
{
_accounts.SetEmail(User.Identity.Name, email);
return RedirectToAction("Index");
}
}
Fixed Codepublic class AccountController : Controller
{
[HttpPost]
[ValidateAntiForgeryToken] // the form must carry the token
public IActionResult UpdateEmail(string email)
{
_accounts.SetEmail(User.Identity.Name, email);
return RedirectToAction("Index");
}
}How to fix
Register AutoValidateAntiforgeryTokenAttribute as a global filter in AddControllersWithViews(o => o.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())) and emit the token in forms with the asp-antiforgery tag helper (on by default for <form>), or tag each unsafe action with [ValidateAntiForgeryToken]. Exempt token-authenticated endpoints explicitly with [IgnoreAntiforgeryToken].
CQLinq source
This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.
// <Name>Review state-changing MVC actions without anti-forgery validation</Name>
// <Id>ND3119:ReviewStateChangingMvcActionsWithoutAntiForgeryValidation</Id>
warnif count > 0
// Only view-based MVC controllers are concerned: API controllers authenticate with tokens, not cookies.
// Third-party types are part of the code model only when the application uses them.
let mvcControllerTypes = ThirdParty.Types.WithFullNameIn("Microsoft.AspNetCore.Mvc.Controller", "System.Web.Mvc.Controller").ToArray()
where mvcControllerTypes.Any()
let validateAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute",
"Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute",
"System.Web.Mvc.ValidateAntiForgeryTokenAttribute").ToHashSetEx()
let ignoreAttrs = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute").ToHashSetEx()
let apiControllerAttrs = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Mvc.ApiControllerAttribute").ToHashSetEx()
let consumesAttrs = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Mvc.ConsumesAttribute").ToHashSetEx()
let stateChangingVerbAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Mvc.HttpPostAttribute", "Microsoft.AspNetCore.Mvc.HttpPutAttribute",
"Microsoft.AspNetCore.Mvc.HttpDeleteAttribute", "Microsoft.AspNetCore.Mvc.HttpPatchAttribute",
"System.Web.Mvc.HttpPostAttribute", "System.Web.Mvc.HttpPutAttribute", "System.Web.Mvc.HttpDeleteAttribute").ToHashSetEx()
// A global filter validates every unsafe request: nothing to report.
let appHasGlobalValidation = Application.Methods.Any(m => validateAttrs.Any(a => m.IsUsing(a)))
where !appHasGlobalValidation
let tagged = new Func<IAttributeTarget, HashSet<IType>, bool>((x, attrs) => x.AttributeClassesThatTagMe.Any(a => attrs.Contains(a)))
from t in Application.Types.ThatDeriveFromAny(mvcControllerTypes)
where !t.IsAbstract && !t.IsGeneratedByCompiler
let classChain = t.BaseClasses.Concat(new[] { t }).ToArray()
where !classChain.Any(c => tagged(c, validateAttrs) || tagged(c, ignoreAttrs) || tagged(c, apiControllerAttrs) || tagged(c, consumesAttrs))
let unvalidatedActions = t.Methods.Where(m =>
m.IsPublic && !m.IsStatic && !m.IsGeneratedByCompiler &&
tagged(m, stateChangingVerbAttrs) &&
!tagged(m, validateAttrs) && !tagged(m, ignoreAttrs) &&
!tagged(m, consumesAttrs)).ToArray() // [Consumes("application/json")]: not reachable from an HTML form
where unvalidatedActions.Length > 0
select new {
t,
unvalidatedActions,
Debt = (10 * unvalidatedActions.Length).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The MVC controller *{0}* exposes state-changing action(s) *{1}* (POST, PUT,
// DELETE or PATCH) without *[ValidateAntiForgeryToken]*: a malicious page can
// submit a form to them with the victim's cookies (Cross-Site Request Forgery).
//</Expl>
//<Desc>
// Matches view-based MVC controllers (deriving from
// *Microsoft.AspNetCore.Mvc.Controller* or *System.Web.Mvc.Controller*)
// whose POST/PUT/DELETE/PATCH actions are not covered by
// *[ValidateAntiForgeryToken]* or *[AutoValidateAntiforgeryToken]* on the
// action, the class chain, or a global filter, and are not explicitly
// exempted with *[IgnoreAntiforgeryToken]*. Controllers or actions tagged
// *[ApiController]* or *[Consumes(...)]* are skipped: they are not
// reachable from an HTML form. Cookie-authenticated form posts without a
// token are the textbook CSRF (CWE-352).
//
// **Flagged Code**
// ```csharp
// public class AccountController : Controller
// {
// [HttpPost] // flagged: no anti-forgery validation
// public IActionResult UpdateEmail(string email)
// {
// _accounts.SetEmail(User.Identity.Name, email);
// return RedirectToAction("Index");
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class AccountController : Controller
// {
// [HttpPost]
// [ValidateAntiForgeryToken] // the form must carry the token
// public IActionResult UpdateEmail(string email)
// {
// _accounts.SetEmail(User.Identity.Name, email);
// return RedirectToAction("Index");
// }
// }
// ```
//</Desc>
//<HowToFix>
// Register *AutoValidateAntiforgeryTokenAttribute* as a global filter in
// *AddControllersWithViews(o => o.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()))*
// and emit the token in forms with the *asp-antiforgery* tag helper (on by
// default for *<form>*), or tag each unsafe action with
// *[ValidateAntiForgeryToken]*. Exempt token-authenticated endpoints
// explicitly with *[IgnoreAntiforgeryToken]*.
//</HowToFix>
Open in the interactive Rules Explorer · All default rules
Security: all 39 items
- Software Composition Analysis (SCA) ND3120
- Transitive Software Composition Analysis (SCA) ND3121
- Don't hard-code secrets in constants ND3114
- Review credentials built from hard-coded values ND3115
- Don't use insecure deserializers ND3110
- Don't use weak hash algorithms (MD5, SHA1) ND3109
- Don't use DES/3DES/RC2 weak cipher algorithms ND3102
- Don't use weak password-based key derivation ND3123
- Don't use the ECB encryption mode ND3132
- Don't disable certificate validation ND3103
- Don't hard-code TLS/SSL protocol versions ND3122
- Review commands vulnerable to SQL injection ND3107
- Review data adapters vulnerable to SQL injection ND3108
- Review raw SQL built from composed strings ND3128
- Review LDAP filters composed from strings ND3137
- Don't enable external XML entity resolution (XXE) ND3111
- Review processes started with a composed arguments string ND3124
- Review web endpoints building file paths from their parameters ND3129
- Review custom archive extraction (Zip Slip) ND3135
- Review assemblies and types loaded from runtime-provided names ND3125
- Review web endpoints without [Authorize] nor [AllowAnonymous] ND3118
- Review state-changing endpoints opened to anonymous callers ND3130
- Review state-changing MVC actions without anti-forgery validation ND3119
- Review HTML-encoding bypasses ND3133
- Review explicitly disabled request validation ND3134
- Review redirects to a URL that is not validated as local ND3126
- Review request-influenced outbound HTTP calls (SSRF) ND3136
- Review insecure ASP.NET Core CORS policies and unguarded developer exception page ND3116
- Review cookies created without HttpOnly and Secure flags ND3117
- Review regular expressions without a match timeout (ReDoS) ND3112
- Review secrets compared with non-constant-time equality ND3113
- Review secrets passed to logging methods ND3127
- Review security operations that log nothing ND3131
- Don't use System.Random for security purposes ND3101
- Don't use CoSetProxyBlanket and CoInitializeSecurity ND3100
- Review publicly visible event handlers ND3104
- Pointers should not be publicly visible ND3105
- Seal methods that satisfy non-public interfaces ND3106
- Restrict sensitive APIs to reviewed components ND3138