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

  1. Add [ValidateAntiForgeryToken] to every individual state-changing action, or apply [AutoValidateAntiforgeryToken] to the entire controller class (preferred — prevents accidentally missing new actions).
  2. In the corresponding Razor views, include @Html.AntiForgeryToken() inside every <form> tag, or use the asp-antiforgery="true" tag helper which renders the token automatically.
  3. For Razor Pages, CSRF protection is enabled by default — do not disable it.
  4. 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.
  5. Configure AutoValidateAntiforgeryTokenAttribute as a global filter in Startup.cs / Program.cs so 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");
}

References