Back to blog
Security & Complianceintermediate

CSRF β€” Cross-Site Request Forgery and How to Prevent It

How CSRF attacks work step-by-step, SameSite cookies as the modern defense, CSRF tokens, ASP.NET Core's built-in protection, and why JWT APIs are naturally immune.

LearnixoApril 15, 20266 min read
SecurityCSRFASP.NET CoreC#.NETCookies
Share:𝕏

The Attack β€” Step by Step

CSRF exploits the browser's automatic cookie behavior. Here's a concrete attack:

  1. You log in to bank.example.com. The bank sets a session cookie.
  2. You visit evil.com in the same browser (another tab, or you were tricked into clicking a link).
  3. evil.com contains this hidden HTML:
HTML
<!-- On evil.com -->
<form id="transfer" action="https://bank.example.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker-account" />
  <input type="hidden" name="amount" value="5000" />
</form>
<script>document.getElementById('transfer').submit();</script>
  1. The browser submits this form to bank.example.com. Because you have a session cookie for that domain, the browser automatically includes it.
  2. The bank's server receives a valid, authenticated POST request β€” it looks identical to a legitimate transfer.
  3. $5,000 transferred. You never saw a form.

Why This Works

The fundamental issue is that browsers send cookies for a domain with every request to that domain, regardless of where the request originated. This is essential for how the web works β€” but it means any site can trigger authenticated requests on your behalf.

CSRF does not require JavaScript. The HTML <form> tag alone is enough. A single <img src="https://bank.example.com/logout"> can trigger a GET-based logout.

SameSite Cookie Attribute β€” The Modern Defense

SameSite is a cookie attribute that tells the browser when to include the cookie in cross-site requests.

C#
Response.Cookies.Append("session", token, new CookieOptions
{
    HttpOnly = true,
    Secure = true,
    SameSite = SameSiteMode.Strict  // or Lax
});

SameSite=Strict β€” the cookie is never sent on cross-site requests. Clicking a link from an email to your bank will not include the cookie (you'd need to reload the page). Highest protection, slightly awkward UX.

SameSite=Lax β€” cookies are sent on top-level GET navigations (clicking a link), but not on cross-origin POST, <img>, <iframe>, etc. Prevents the form-based attack above. This is the browser default in modern browsers.

SameSite=None β€” always sent cross-site (must also set Secure). Required for embedded widgets, cross-origin iframes, payment flows.

For most applications, SameSite=Lax (or Strict) is sufficient and eliminates CSRF without any other measures.

CSRF Tokens β€” The Classic Defense

Before SameSite was widely supported, CSRF tokens were the standard defense. They're still appropriate for defense in depth.

Synchronizer Token Pattern: the server generates a unique, secret token per session. Every state-changing form includes the token. On submission, the server validates it. An attacker on evil.com cannot read the token (same-origin policy blocks cross-origin reads), so they can't forge a valid request.

C#
// ASP.NET Core generates and validates CSRF tokens automatically in Razor Pages
// For controllers, use the attribute:

[ApiController]
[AutoValidateAntiforgeryToken]  // Applies to all non-GET actions
public class AccountController : Controller
{
    [HttpPost("transfer")]
    [ValidateAntiForgeryToken]  // Explicit on a specific action
    public async Task<IActionResult> Transfer(TransferRequest request)
    {
        // If CSRF token is missing or invalid, returns 400 automatically
        await _bankService.TransferAsync(request);
        return Ok();
    }
}

In Razor views, @Html.AntiForgeryToken() or <form asp-controller="Account" asp-action="Transfer"> automatically injects a hidden field:

HTML
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8N..." />

For AJAX requests, include the token in a request header:

JAVASCRIPT
// Read the token from the cookie or a meta tag
const token = document.querySelector('meta[name="csrf-token"]')?.content;

fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': token
    },
    body: JSON.stringify({ to: 'recipient', amount: 100 })
});
C#
// Configure ASP.NET Core to read token from header
services.AddAntiforgery(options =>
{
    options.HeaderName = "X-CSRF-TOKEN";
});

Double Submit Cookie Pattern

When you can't share session state (stateless APIs behind load balancers), use the Double Submit Cookie pattern:

  1. Server sets a random value in a cookie: csrf-token=abc123
  2. Client reads this cookie (it must be readable β€” not HttpOnly) and sends it as a request header or body parameter
  3. Server verifies the cookie value matches the submitted value

An attacker on evil.com cannot read your cookie (same-origin policy), so they cannot include the matching value. The check proves the request originated from your frontend.

C#
// Middleware: set CSRF cookie on first request
app.Use(async (context, next) =>
{
    if (!context.Request.Cookies.ContainsKey("csrf-token"))
    {
        var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
        context.Response.Cookies.Append("csrf-token", token, new CookieOptions
        {
            HttpOnly = false,  // Must be readable by JavaScript
            Secure = true,
            SameSite = SameSiteMode.Strict
        });
    }
    await next();
});

JWT in Authorization Header β€” Not Vulnerable to CSRF

This is an important nuance developers often miss:

If your API uses JWT in the Authorization: Bearer <token> header (not a cookie), it is not vulnerable to CSRF.

Here's why: the browser never automatically adds custom headers to cross-origin requests. When evil.com submits a form to your API, the request arrives without an Authorization header. Your API sees no token and returns 401.

CSRF is exclusively a cookie problem. APIs that authenticate via:

  • Authorization: Bearer header
  • API keys in custom headers
  • Basic Auth in the header

...are immune to CSRF. You only need CSRF protection when cookies are your authentication mechanism.

CORS as a Partial Defense

CORS (Cross-Origin Resource Sharing) controls which origins can make cross-origin requests from JavaScript. A strict CORS policy prevents evil.com's JavaScript from making authenticated fetch/XHR calls to your API and reading the response.

However, CORS does not block cross-origin form submissions. HTML forms are not subject to CORS β€” they can POST to any origin (the response is just not readable). So CORS alone does not protect against CSRF.

C#
// Strict CORS β€” only allow your frontend origin
builder.Services.AddCors(options =>
{
    options.AddPolicy("Frontend", policy =>
        policy.WithOrigins("https://app.example.com")
              .AllowCredentials()
              .AllowedMethods(["GET", "POST", "PUT", "DELETE"])
    );
});

CORS + SameSite cookies together provide strong protection. Neither alone is sufficient for cookie-based auth.

What Does NOT Protect Against CSRF

  • HTTPS β€” encryption protects in-transit data but doesn't stop the browser from sending cookies cross-origin
  • Checking the Referer header β€” can be spoofed, can be suppressed by privacy settings, unreliable
  • POST-only endpoints β€” forms can POST. Method alone provides no protection
  • Secret query parameters β€” query strings are visible in server logs and browser history

Defense Recommendation

For modern applications:

  1. Use SameSite=Lax or SameSite=Strict on all session/auth cookies β€” this alone prevents most CSRF
  2. Add CSRF tokens for defense in depth in Razor applications
  3. If using JWT in Authorization headers, no CSRF protection is needed for those endpoints
  4. Pair with strict CORS to block cross-origin JS reads

If you're building a new API that uses JWT in the Authorization header, CSRF is not your concern. If you have a Razor/MVC app or SPAs that use cookie-based auth, use SameSite + ASP.NET Core's antiforgery.

Enjoyed this article?

Explore the Security & Compliance learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.