VNX-CS-005 – C# Missing ValidateAntiForgeryToken on State-Changing MVC Actions
Overview
This rule identifies ASP.NET MVC controller action methods that perform state-changing HTTP operations — [HttpPost], [HttpPut], [HttpDelete], or [HttpPatch] — without the [ValidateAntiForgeryToken] attribute in the surrounding context. The rule also checks for [AutoValidateAntiforgeryToken] at the controller level and [IgnoreAntiforgeryToken] (which suppresses the check intentionally), and Consumes( attributes used for API endpoints with explicit content-type negotiation.
Cross-Site Request Forgery (CSRF) exploits the fact that browsers automatically attach cookies — including session cookies — to all requests sent to a domain, regardless of which site triggered the request. A malicious page on a different origin can silently submit forms or fire AJAX requests to a victim’s authenticated session on a target site, causing state changes the user did not intend.
The [ValidateAntiForgeryToken] attribute stops CSRF by requiring the request to include a value that was generated by the server, embedded in the HTML page, and known only to the legitimate client. A cross-origin attacker cannot read the token from the victim’s browser because the Same-Origin Policy blocks cross-origin reads.
Severity: Medium | CWE: CWE-352 – Cross-Site Request Forgery (CSRF)
Why This Matters
CSRF attacks are invisible to the victim. An attacker embeds a hidden form or an <img> tag with a carefully crafted src attribute in a page the victim visits — social media, email HTML, or an ad banner are all valid vectors. When the victim’s browser loads that page while logged into the target site, the attack executes silently: a fund transfer is initiated, an email address is changed, a password reset is triggered, or an admin account is created.
Banking applications, e-commerce sites, and SaaS admin panels are the most common targets. CSRF vulnerabilities in administrative actions are particularly severe: an attacker who tricks a site administrator into visiting a malicious link can create new admin users, change billing details, delete content, or take over the entire application — all without ever obtaining the admin’s password or cookie value.
Modern SPAs that use Authorization: Bearer headers with JWT tokens are not vulnerable to CSRF because browsers do not automatically attach custom headers. However, classic MVC applications that rely on cookie-based session authentication — the default in ASP.NET MVC — require CSRF protection on every state-changing endpoint.
What Gets Flagged
// FLAGGED: [HttpPost] action without [ValidateAntiForgeryToken]
[HttpPost]
public ActionResult TransferFunds(TransferViewModel model)
{
_bankService.Transfer(model.FromAccount, model.ToAccount, model.Amount);
return RedirectToAction("Index");
}
// FLAGGED: [HttpDelete] action without token validation
[HttpDelete]
public IActionResult DeleteUser(int id)
{
_userService.Delete(id);
return NoContent();
}
Remediation
- Add
[ValidateAntiForgeryToken]to every individual state-changing action, or apply[AutoValidateAntiforgeryToken]to the entire controller class (preferred — prevents accidentally missing new actions). - In the corresponding Razor views, include
@Html.AntiForgeryToken()inside every<form>tag, or use theasp-antiforgery="true"tag helper which renders the token automatically. - For Razor Pages, CSRF protection is enabled by default — do not disable it.
- For API controllers that use bearer tokens instead of cookies, exclude them explicitly with
[IgnoreAntiforgeryToken]or move them to a separate controller that does not use cookie authentication. - Configure
AutoValidateAntiforgeryTokenAttributeas a global filter inStartup.cs/Program.csso all controllers are protected by default.
// SAFE: AutoValidateAntiforgeryToken at controller level covers all actions
[AutoValidateAntiforgeryToken]
public class AccountController : Controller
{
[HttpPost]
public ActionResult TransferFunds(TransferViewModel model)
{
_bankService.Transfer(model.FromAccount, model.ToAccount, model.Amount);
return RedirectToAction("Index");
}
}
// SAFE: token explicitly validated on individual action
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult DeleteUser(int id)
{
_userService.Delete(id);
return RedirectToAction("Users");
}