diff --git a/aspnetcore/blazor/forms/index.md b/aspnetcore/blazor/forms/index.md index 848401696551..a7981b3868b0 100644 --- a/aspnetcore/blazor/forms/index.md +++ b/aspnetcore/blazor/forms/index.md @@ -319,7 +319,7 @@ The the `DisableCsrfProtection` configuration setting can be supplied by any con > [!WARNING] > Disabling the automatic CSRF protection middleware removes the default header-based (`Sec-Fetch-Site`/`Origin`) protection for the entire app. Only disable it if you provide an alternative CSRF defense, such as explicitly adopting token-based antiforgery middleware by calling . -For more information, see . +For more information, see [Automatic CSRF protection](xref:security/anti-request-forgery#automatic-csrf-protection-in-aspnet-core). > [!IMPORTANT] > The following guidance on the component and the service only apply to an app that explicitly adopts token-based Antiforgery Middleware by calling in its request processing pipeline. diff --git a/aspnetcore/blazor/security/index.md b/aspnetcore/blazor/security/index.md index 4a402e761278..544b432b32e0 100644 --- a/aspnetcore/blazor/security/index.md +++ b/aspnetcore/blazor/security/index.md @@ -101,7 +101,7 @@ The the `DisableCsrfProtection` configuration setting can be supplied by any con > [!WARNING] > Disabling the automatic CSRF protection middleware removes the default header-based (`Sec-Fetch-Site`/`Origin`) protection for the entire app. Only disable it if you provide an alternative CSRF defense, such as explicitly adopting token-based antiforgery middleware by calling . -For more information, see . +For more information, see [Automatic CSRF protection](xref:security/anti-request-forgery#automatic-csrf-protection-in-aspnet-core). > [!IMPORTANT] > The following guidance on the component and the service only apply to an app that explicitly adopts token-based antiforgery middleware by calling in its request processing pipeline. diff --git a/aspnetcore/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection.md b/aspnetcore/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection.md index 311ea7d00a10..7d2aaec0f2f0 100644 --- a/aspnetcore/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection.md +++ b/aspnetcore/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection.md @@ -54,7 +54,7 @@ app.UseAntiforgery(); Apps that call `app.UseAntiforgery()`, directly or implicitly through `AddRazorComponents`, require no changes. -If you intentionally removed `app.UseAntiforgery()` and want to rely on the automatic cross-origin CSRF protection instead, no action is required. Be aware that antiforgery tokens are no longer generated for your forms, and cross-origin form posts are rejected based on `Sec-Fetch-Site` and `Origin` rather than tokens. For more information, see and . +If you intentionally removed `app.UseAntiforgery()` and want to rely on the automatic cross-origin CSRF protection instead, no action is required. Be aware that antiforgery tokens are no longer generated for your forms, and cross-origin form posts are rejected based on `Sec-Fetch-Site` and `Origin` rather than tokens. For more information, see and . ## Affected APIs diff --git a/aspnetcore/migration/100-to-110.md b/aspnetcore/migration/100-to-110.md index b3b84cc3a065..3c70bfa2cd8e 100644 --- a/aspnetcore/migration/100-to-110.md +++ b/aspnetcore/migration/100-to-110.md @@ -3,7 +3,7 @@ title: Migrate from ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11 author: wadepickett description: Learn how to migrate an ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11. ms.author: wpickett -ms.date: 06/09/2026 +ms.date: 07/15/2026 uid: migration/100-to-110 --- # Migrate from ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11 @@ -73,6 +73,10 @@ In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.or [!INCLUDE[](~/migration/100-to-110/includes/blazor.md)] +## Security + +[!INCLUDE[](~/migration/100-to-110/includes/security.md)] + ## Breaking changes Use the articles in [Breaking changes in .NET](/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET. diff --git a/aspnetcore/migration/100-to-110/includes/security.md b/aspnetcore/migration/100-to-110/includes/security.md new file mode 100644 index 000000000000..74cbf88cee45 --- /dev/null +++ b/aspnetcore/migration/100-to-110/includes/security.md @@ -0,0 +1,22 @@ +### Automatic CSRF protection + +.NET 11 adds automatic Cross-Site Request Forgery (CSRF) protection. When an app is built with `WebApplication.CreateBuilder` and has endpoints, a middleware is wired up by default that inspects the `Sec-Fetch-Site` and `Origin` headers and records a validation verdict on the request. + +The middleware validates endpoints that opt in to antiforgery validation—that is, endpoints with metadata implementing `IAntiforgeryMetadata` where `RequiresValidation` is `true`. The framework sets this automatically for: + +* All Blazor server-side rendering (SSR) endpoints. Each is protected by default; a page can opt out with `@attribute [RequireAntiforgeryToken(false)]`. +* Minimal API endpoints that bind form data. +* MVC actions that use antiforgery validation, such as those annotated with `[ValidateAntiForgeryToken]` or `[AutoValidateAntiforgeryToken]`. + +Endpoints that bind JSON, such as a plain `MapPost` or a Web API `[HttpPost]` action, have no behavioral change. + +Going forward, the automatic CSRF protection is the recommended defense, and most apps no longer need the token-based antiforgery system. Keep the token-based system when the app must support browsers that don't send `Sec-Fetch-Site`, uses , or must keep the token defense as an independent layer for a compliance requirement. Both protections can coexist. + +To simplify an app that configures antiforgery explicitly, drop the and calls and rely on the automatic protection. For most apps, this is a one-line change with no other code updates. For Blazor static SSR, removing `app.UseAntiforgery()` also stops antiforgery token generation for rendered forms; see [Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection). + +A `400 Bad Request` on a cross-origin form post is the CSRF protection working as intended. When the request comes from a legitimate origin, allow that origin rather than suppressing the check: + +* Configure [CORS](xref:security/cors) so the endpoint's resolved policy includes the caller's origin. The CSRF middleware honors that policy and allows the request. +* Only opt an endpoint out with `.DisableAntiforgery()` (minimal APIs) or `[IgnoreAntiforgeryToken]` (MVC) when it isn't vulnerable to CSRF, such as an endpoint that isn't reachable from a browser or that authenticates with a non-cookie mechanism (for example, bearer authentication). + +For a full description of the middleware, the validation rules, and how it interacts with the token-based antiforgery system, see [Automatic CSRF protection in ASP.NET Core](xref:security/anti-request-forgery#automatic-csrf-protection-in-aspnet-core). diff --git a/aspnetcore/migration/antiforgery-to-csrf.md b/aspnetcore/migration/antiforgery-to-csrf.md deleted file mode 100644 index 7e5aa5edeace..000000000000 --- a/aspnetcore/migration/antiforgery-to-csrf.md +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: Adopt automatic CSRF protection in .NET 11 -ai-usage: ai-assisted -author: tdykstra -description: Use the automatic CSRF protection introduced in .NET 11 to simplify or replace the token-based antiforgery system in an existing ASP.NET Core app. -monikerRange: '>= aspnetcore-11.0' -ms.author: tdykstra -ms.date: 06/25/2026 -uid: migration/antiforgery-to-csrf ---- -# Adopt automatic CSRF protection in .NET 11 - -.NET 11 ships an automatic Cross-Site Request Forgery (CSRF) protection middleware that validates `Sec-Fetch-Site` and `Origin` headers on every request. It's enabled by default and, for many modern apps, is sufficient on its own. This article helps app authors who are upgrading from .NET 10 decide whether to simplify their existing antiforgery setup, and walks through the steps to do so. - -For a full reference on how the new middleware works, see . - -If something stops working after the upgrade — most commonly cross-origin requests from a Single Page App (SPA) that now return HTTP `400` — see [After upgrade: if requests start failing](#after-upgrade-if-requests-start-failing). - -## Should I keep the token-based antiforgery system? - -Most apps that use the token-based system have `app.UseAntiforgery()` and the `__RequestVerificationToken` plumbing in place. The new middleware doesn't remove or replace any of that — the two protections coexist. The choice is whether to keep both as defense in depth, or simplify by removing the older token-based system. - -**Keep the token-based system when:** - -* The app must support browsers that don't send `Sec-Fetch-Site`. See the [Browser support](xref:security/csrf-protection#browser-support) section of the reference doc for the baseline. -* A security review or compliance requirement specifies the token defense as an independent layer. -* The app uses `IAntiforgeryAdditionalDataProvider` to round-trip extra data inside the antiforgery token. - -**Consider dropping the token-based system when:** - -* The app targets modern evergreen browsers only. -* Defense in depth at the token layer isn't a hard requirement. - -If unsure, keep both. They're complementary, and there's no functional conflict between them. - -## Simplify: drop the token-based system - -For apps that fit the second list above, the automatic CSRF middleware alone is enough to defend against the classic CSRF attack. Dropping the token-based system removes the token round-trip, the [Data Protection](xref:security/data-protection/introduction) dependency that the antiforgery system uses for token encryption, and the `IAntiforgery` calls in views and page handlers. - -### What to remove - -| Item | Where it appears | Reason it can go | -|---|---|---| -| `app.UseAntiforgery()` | `Program.cs` | The automatic CSRF middleware is registered by default. | -| `services.AddAntiforgery(...)` (when used only to configure options) | `Program.cs` | Only needed when the token system is in use. | -| `@Html.AntiForgeryToken()`, `asp-antiforgery` | Razor views / pages | No token to render. | -| `RequestVerificationToken` header in JavaScript / `fetch` calls | Client code | Browser-supplied `Sec-Fetch-Site` and `Origin` replace it. | -| `[ValidateAntiForgeryToken]`, `[AutoValidateAntiforgeryToken]` | MVC controllers | These are token-specific attributes. Without `UseAntiforgery()` they have no effect. | -| `[RequireAntiforgeryToken]` attribute / `RequireAntiforgeryTokenAttribute` metadata | MVC actions and Minimal API endpoints | Token-specific metadata. | - -Per-endpoint opt-outs (`.DisableAntiforgery()` on Minimal APIs, `[IgnoreAntiforgeryToken]` on MVC) can stay where they are — both also opt the endpoint out of the new CSRF middleware. Remove them only if the endpoint should be protected after the simplification. - -For Blazor static server-side rendering (SSR), removing `app.UseAntiforgery()` is a breaking change that also stops antiforgery token generation. See [Blazor static server-side rendering](#blazor-static-server-side-rendering) before removing it. - -### Before / after - -Minimal API: - -```csharp -// .NET 10 (token-based) -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddAntiforgery(); - -var app = builder.Build(); -app.UseAntiforgery(); - -app.MapPost("/api/widgets", (Widget w) => Results.Created($"/api/widgets/{w.Id}", w)); - -app.Run(); -``` - -```csharp -// .NET 11 (automatic CSRF only) -var builder = WebApplication.CreateBuilder(args); - -var app = builder.Build(); - -app.MapPost("/api/widgets", (Widget w) => Results.Created($"/api/widgets/{w.Id}", w)); - -app.Run(); -``` - -MVC controller: - -```csharp -// .NET 10 -[ApiController] -[Route("api/[controller]")] -[AutoValidateAntiforgeryToken] -public class WidgetsController : ControllerBase -{ - [HttpPost] - public IActionResult Post([FromBody] Widget w) => CreatedAtAction(nameof(Get), new { id = w.Id }, w); -} -``` - -```csharp -// .NET 11 -[ApiController] -[Route("api/[controller]")] -public class WidgetsController : ControllerBase -{ - [HttpPost] - public IActionResult Post([FromBody] Widget w) => CreatedAtAction(nameof(Get), new { id = w.Id }, w); -} -``` - -Cross-origin SPAs still need a CORS policy listing the trusted origin, regardless of which antiforgery model the app uses. See [Allowing cross-origin clients](xref:security/csrf-protection#allowing-cross-origin-clients) for the resolution rules. - -### Blazor static server-side rendering - -Removing `app.UseAntiforgery()` is a breaking change for Blazor static server-side rendering (SSR). Without the token-based middleware: - -* Form posts are validated by the automatic CSRF middleware using `Sec-Fetch-Site` and `Origin` instead of antiforgery tokens. Blazor SSR endpoints now trust the verdict recorded by the upstream middleware rather than validating the request themselves. -* Blazor stops generating antiforgery tokens for rendered forms, because no middleware is present to validate a token on a later request. - -This is appropriate for apps that target modern browsers and rely on the header-based defense. Apps that need antiforgery tokens — for example, to support browsers that don't send `Sec-Fetch-Site` — should keep `app.UseAntiforgery()`. For the formal breaking-change notice, see [Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection). - -## After upgrade: if requests start failing - -The automatic CSRF middleware is enabled by default in .NET 11. Same-origin browser requests and non-browser clients (`curl`, server-to-server, mobile apps) are unaffected. Cross-origin browser form posts that aren't covered by a CORS policy are rejected with HTTP `400` when the endpoint processes the form. - -### Symptoms - -Cross-origin browser form posts that succeeded on .NET 10 fail on .NET 11 with HTTP `400 Bad Request` and an empty response body when the endpoint binds or reads the form. The form processing is rejected before the handler body runs. Endpoints that don't read form data — such as JSON APIs — aren't affected, because the recorded verdict is only enforced by form consumers. - -The same request issued from `curl` or another non-browser client typically succeeds, which is a useful quick check that distinguishes the new middleware from other causes: - -```bash -# In a browser at https://app.contoso.com posting a form to https://api.contoso.com: 400 -# From a terminal, the same request: 200/201 (no Sec-Fetch-Site, no Origin → treated as non-browser) -curl -i -X POST https://api.contoso.com/widgets \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "name=test" -``` - -### Confirm with logs - -To confirm the rejection comes from the CSRF middleware, enable `Debug` logging for `Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware` in `appsettings.Development.json`: - -```json -{ - "Logging": { - "LogLevel": { - "Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware": "Debug" - } - } -} -``` - -A recorded verdict appears as: - -```text -dbug: Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware[1] - Cross-origin CSRF protection marked request POST /widgets from origin 'https://app.contoso.com' as invalid. -``` - -### Fix A: allow the origin via CORS - -The most common fix is to declare the calling origin in a CORS policy. The CSRF middleware reuses the policy that the CORS middleware resolves for the endpoint, so registering the origin via `AddDefaultPolicy` or `AddPolicy` is enough to allow it. - -```csharp -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddCors(options => -{ - options.AddDefaultPolicy(policy => - policy.WithOrigins("https://app.contoso.com") - .AllowAnyHeader() - .AllowAnyMethod()); -}); - -var app = builder.Build(); -app.UseCors(); - -app.MapPost("/widgets", (Widget w) => Results.Created($"/widgets/{w.Id}", w)); - -app.Run(); -``` - -For per-endpoint policies, use `.RequireCors("name")` (Minimal API) or `[EnableCors("name")]` (MVC). For the full resolution priority — and an important caveat: `AllowAnyOrigin` is **not** honored as a CSRF trust signal — see [Allowing cross-origin clients](xref:security/csrf-protection#allowing-cross-origin-clients) in the reference doc. For details on CORS itself, see . - -### Fix B: opt the endpoint out - -If an endpoint isn't browser-reachable, or is secured by non-cookie credentials (bearer tokens, API keys), opt it out instead of opening it through CORS: - -```csharp -// Minimal API -app.MapPost("/api/webhook", (WebhookPayload p) => Results.Accepted()) - .DisableAntiforgery(); -``` - -```csharp -// MVC -[ApiController] -[Route("api/[controller]")] -[IgnoreAntiforgeryToken] -public class WebhookController : ControllerBase -{ - [HttpPost] - public IActionResult Post([FromBody] WebhookPayload payload) => Accepted(); -} -``` - -> [!WARNING] -> Don't opt out browser-accessible endpoints that rely on cookies for authentication. Reserve the opt-out for endpoints that aren't callable from a browser, or are secured by non-cookie authentication such as bearer tokens or API keys. - -### Escape hatch: disable globally - -If the upgrade window is tight and individual fixes will take time, the middleware can be turned off across the entire app with the `DisableCsrfProtection` configuration key: - -```json -{ - "DisableCsrfProtection": true -} -``` - -This is an escape hatch, not a recommended end state. Re-enable as soon as CORS and per-endpoint opt-outs are in place. - -> [!WARNING] -> The automatic CSRF middleware also satisfies the antiforgery requirement for endpoints that require validation, even when an app doesn't call `app.UseAntiforgery()`. If an app relies on antiforgery but doesn't call `app.UseAntiforgery()`, disabling the CSRF middleware with `DisableCsrfProtection` removes the only antiforgery middleware in the pipeline. A request to an endpoint that requires validation then throws an exception. The same is true when the app runs on a host that isn't built with `WebApplication`, where the CSRF middleware isn't injected. In either configuration, add `app.UseAntiforgery()` so a middleware is present to validate the request. - -## Related - -* — full reference for the new middleware. -* — the token-based antiforgery system. -* — CORS configuration reference. -* [Breaking change: Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection) -* — overall .NET 10 → .NET 11 migration guide. diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md index 5124ae64cde8..d3a5884b0215 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md @@ -790,16 +790,16 @@ For more information, see [Fix TempData and SupplyParameterFromSession persisten ### Redundant `app.UseAntiforgery()` removed from Blazor Web App templates -[CSRF protection](xref:security/csrf-protection) is enabled by default via the auto-injected CSRF Protection Middleware, so the explicit `app.UseAntiforgery()` call in Blazor Web App templates has been removed. +[CSRF protection](xref:security/anti-request-forgery#automatic-csrf-protection-in-aspnet-core) is enabled by default via the auto-injected CSRF protection middleware, so the explicit `app.UseAntiforgery()` call in Blazor Web App templates has been removed. For more information, see the following resources: -* +* * [Blazor server-side rendering defers antiforgery validation to middleware (breaking change announcement)](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection) General coverage for the new automatic CSRF protection in ASP.NET Core: -* +* * * diff --git a/aspnetcore/security/anti-request-forgery.md b/aspnetcore/security/anti-request-forgery.md index 2a6ca614260d..a6d226e4a0d2 100644 --- a/aspnetcore/security/anti-request-forgery.md +++ b/aspnetcore/security/anti-request-forgery.md @@ -7,7 +7,7 @@ description: Discover how to prevent attacks against web apps where a malicious monikerRange: '>= aspnetcore-3.1' ms.author: tdykstra ms.custom: mvc -ms.date: 06/18/2026 +ms.date: 07/15/2026 uid: security/anti-request-forgery --- # Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core @@ -83,6 +83,336 @@ Although `example1.contoso.net` and `example2.contoso.net` are different hosts, Attacks that exploit trusted cookies between apps hosted on the same domain can be prevented by not sharing domains. When each app is hosted on its own domain, there's no implicit cookie trust relationship to exploit. +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + +### Fetch metadata headers + +Modern browsers attach [Fetch Metadata](https://developer.mozilla.org/docs/Web/HTTP/Headers/Sec-Fetch-Site) request headers—most importantly `Sec-Fetch-Site`—to every request. `Sec-Fetch-Site` describes the relationship between the origin that initiated the request and the origin being requested: `same-origin` identifies a request the site made to itself, while `same-site` and `cross-site` identify requests initiated by another origin. The [`Origin`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Origin) header carries the initiating origin and serves as a fallback for browsers that predate Fetch Metadata. + +`Sec-Fetch-Site` and `Origin` are [forbidden request headers](https://developer.mozilla.org/docs/Glossary/Forbidden_request_header): the browser sets them, and JavaScript running in a page can't override or forge them. That makes them a trustworthy signal for telling a site's own requests apart from cross-site requests without a server-issued token. The [automatic CSRF protection](#automatic-csrf-protection-in-aspnet-core) built into ASP.NET Core uses this signal to reject cross-site form posts that aren't explicitly trusted. + +## Automatic CSRF protection in ASP.NET Core + +ASP.NET Core ships an automatic CSRF protection middleware that's enabled by default in apps built with `WebApplication.CreateBuilder`. Unlike the [token-based antiforgery system](#antiforgery-in-aspnet-core), this middleware doesn't issue or validate tokens. Instead, it inspects the `Sec-Fetch-Site` and `Origin` [Fetch metadata headers](#fetch-metadata-headers) and records a validation verdict on the request. Components that process submitted form data enforce that verdict, rejecting cross-origin form posts that aren't explicitly trusted. + +For most apps, no code changes are required: same-origin browser requests, safe HTTP methods, and non-browser clients (`curl`, server-to-server, mobile apps) all pass through unaffected. The middleware primarily affects apps that accept cross-origin form posts from a browser, such as a site that posts a form to an API on a different origin. Those scenarios need to either configure [CORS](xref:security/cors) to declare the trusted origin or opt the endpoint out. + +This middleware is *additive* to the token-based antiforgery system. The two protections coexist and can both be active on the same endpoint. For a comparison of when each one applies, see [Interaction with token-based antiforgery](#interaction-with-token-based-antiforgery). + +### How it works + +For every request, the middleware evaluates a short chain of rules to reach a verdict—*allowed* or *denied*. The checks run in order, and the first match wins: + +1. **Safe HTTP methods are always allowed.** `GET`, `HEAD`, `OPTIONS`, and `TRACE` requests pass through. This follows [RFC 9110 §9.2.1](https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.1) and is consistent with the long-standing rule that endpoints shouldn't change state on `GET`. +1. **`Sec-Fetch-Site: same-origin` or `Sec-Fetch-Site: none` is allowed.** Modern browsers send `Sec-Fetch-Site` on every request. `same-origin` covers normal in-app navigation and fetch, and `none` covers requests initiated directly by the user (typing a URL, using a bookmark). This is the most common code path—most legitimate browser traffic exits here. +1. **A trusted origin from CORS is allowed.** If the request carries an `Origin` header and the endpoint's resolved CORS policy trusts that origin, the request is allowed. The middleware resolves the policy the same way the CORS middleware does: per-endpoint policy from `[EnableCors("name")]` first, then the default policy registered with `AddDefaultPolicy`. See [Allowing cross-origin clients](#allowing-cross-origin-clients) for important limits on this rule. +1. **Any other `Sec-Fetch-Site` value is denied.** When `Sec-Fetch-Site` is `cross-site` or `same-site` and the origin isn't trusted via CORS, the request is denied. +1. **No `Sec-Fetch-Site`, but `Origin` is present:** the middleware compares the `Origin` to `scheme://host[:port]` built from the request. If they match, the request is allowed; otherwise it's denied. This is the fallback path for browsers older than the Fetch Metadata spec (released ~2020). +1. **No `Sec-Fetch-Site` and no `Origin`: the request is allowed.** Browsers always send at least one of these on a write request, so a request missing both is almost certainly a non-browser client such as `curl`, Postman, a mobile app, or a server-to-server caller. CSRF is a browser-only attack vector, so these requests pass through. + +The middleware records this verdict on the request rather than ending the request itself. For how and when a denied verdict turns into an HTTP `400 Bad Request` response, see [Deferred validation](#deferred-validation). + +### Deferred validation + +The middleware doesn't reject a request on its own. Instead, it records its verdict on the request's —the same feature the token-based antiforgery system uses—where a denied verdict is recorded as *invalid*. The request continues down the pipeline. An invalid verdict becomes an HTTP `400 Bad Request` only when a component that processes form data observes it. This deferral matches how the token-based system already behaves: the verdict is produced early but enforced at the point where a form is consumed. + +The following components read and reject a request with `400 - Bad Request` when the recorded verdict is invalid: + +* MVC actions protected by antiforgery. +* Minimal API endpoints that bind a form parameter. +* Blazor SSR endpoints. +* Any code that reads the request form directly, which acts as a backstop. + +Each consumer first confirms that an antiforgery or CSRF middleware actually ran before it trusts the verdict, so a pipeline without either middleware doesn't produce false rejections. + +A consequence of this model is that an endpoint that never reads form data runs even when the verdict is invalid. For example, a JSON API endpoint that binds its body from JSON, or a handler that ignores the request body, isn't rejected automatically on a cross-origin request. The verdict is still recorded on for code that wants to inspect it, but nothing enforces it. CSRF is a form-and-cookie attack vector, so endpoints that don't consume a browser-submitted form generally don't need this rejection. Endpoints that do process forms—Razor Pages, MVC views, Blazor SSR, and minimal API form binding—get the protection automatically. + +### Default behavior + +The middleware is registered automatically by `WebApplication.CreateBuilder` and runs after authentication and authorization. It validates each request using the registered `ICsrfProtection` implementation, which by default applies the rules described in [How it works](#how-it-works). The default implementation can be replaced; see [Customizing: implement `ICsrfProtection`](#customizing-implement-icsrfprotection). To turn the middleware off entirely, see [Disabling globally](#disabling-globally). + +The result is that a minimal app with a form-handling endpoint like the following is already protected: + +```csharp +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +app.MapPost("/widgets", ([FromForm] Widget w) => + Results.Created($"/widgets/{w.Id}", w)); + +app.Run(); +``` + +A browser making a same-origin `POST /widgets` request reaches the endpoint normally. A browser at `https://attacker.example.com` posting the same form is rejected with `400 - Bad Request` when the endpoint binds the form, before the handler body runs. A `curl` request without `Sec-Fetch-Site` or `Origin` is allowed. + +Because rejection is [deferred to form consumers](#deferred-validation), an endpoint that doesn't read form data—such as a JSON API that binds its body from JSON—isn't rejected automatically, even on a cross-origin request. The verdict is still recorded on the request for code that wants to inspect it. + +The middleware integrates with the existing antiforgery model: + +* **Minimal APIs:** Calling `.DisableAntiforgery()` on an endpoint opts that endpoint out of *both* the token-based middleware and CSRF protection middleware. The same metadata (`IAntiforgeryMetadata { RequiresValidation = false }`) is checked by both. +* **MVC controllers and actions:** `[IgnoreAntiforgeryToken]` also opts the endpoint out of both protections. + +### Allowing cross-origin clients + +The most common scenario that requires action is a browser-based client that submits a cross-origin form—for example, a site at `https://app.contoso.com` posting a form to an API at `https://api.contoso.com`. Such form posts are denied by default because `Sec-Fetch-Site` is `same-site` or `cross-site` rather than `same-origin`, and the form consumer enforces that verdict with a `400 - Bad Request`. + +The CSRF middleware doesn't introduce its own trust list. It reuses the same CORS policy that the [CORS middleware](xref:security/cors) resolves for the endpoint: if that policy allows the request's `Origin`, the CSRF middleware records an allowed verdict for the request. + +The policy is picked per-endpoint in this order: + +1. `[EnableCors("api")]` (MVC) or `.RequireCors("api")` (Minimal API) → the named policy `"api"`. +1. No CORS metadata on the endpoint → the default policy registered with `AddDefaultPolicy`. +1. No matching policy (named policy not registered, no default policy, or `services.AddCors()` never called) → no CORS-derived trust. The middleware falls through to the `Sec-Fetch-Site` and Origin-vs-Host rules. + +A minimal example using a default policy and a Minimal API endpoint: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + policy.WithOrigins("https://app.contoso.com") + .AllowAnyHeader() + .AllowAnyMethod()); +}); + +var app = builder.Build(); + +app.UseCors(); + +app.MapPost("/widgets", ([FromForm] Widget w) => + Results.Created($"/widgets/{w.Id}", w)); + +app.Run(); +``` + +For a named policy on a single endpoint: + +```csharp +app.MapPost("/widgets", ([FromForm] Widget w) => Results.Created($"/widgets/{w.Id}", w)) + .RequireCors("api"); +``` + +> [!WARNING] +> `AllowAnyOrigin` is intentionally **not** honored as a CSRF trust signal. `AllowAnyOrigin` means "any browser can read this resource," which is a different concern than "any origin may mutate state on the user's behalf." Treating `AllowAnyOrigin` as trusted would turn this middleware into a no-op for cross-origin writes. Apps that need a public-read CORS policy combined with CSRF-protected writes should list trusted write origins explicitly with `WithOrigins` or [opt write endpoints out](#opting-an-endpoint-out) if they don't rely on cookie-based authentication. + +`[DisableCors]` on an endpoint isn't a CSRF opt-out. It skips the CORS-derived trust step, and the request still has to satisfy the `Sec-Fetch-Site` and Origin-vs-Host rules. To opt out of CSRF protection, see [Opting an endpoint out](#opting-an-endpoint-out). + +For details on configuring CORS itself—`AddCors`, `AddDefaultPolicy`, `AddPolicy`, `WithOrigins`, and the rest of the policy builder API—see . + +### Opting an endpoint out + +If an endpoint isn't browser-reachable or is secured by a non-cookie mechanism, such as a bearer token or API key, opt it out individually rather than disabling the middleware globally. + +**Minimal APIs** — call `DisableAntiforgery` on the endpoint or group: + +```csharp +app.MapPost("/api/webhook", (WebhookPayload p) => Results.Accepted()) + .DisableAntiforgery(); +``` + +**MVC controllers** — apply `[IgnoreAntiforgeryToken]` to the action or controller: + +```csharp +[ApiController] +[Route("api/[controller]")] +[IgnoreAntiforgeryToken] +public class WebhookController : ControllerBase +{ + [HttpPost] + public IActionResult Post([FromBody] WebhookPayload payload) => Accepted(); +} +``` + +Either approach adds `IAntiforgeryMetadata { RequiresValidation = false }` to the endpoint, which the CSRF middleware honors by skipping validation. + +> [!WARNING] +> Disabling CSRF protection on an endpoint should only be done when the endpoint isn't vulnerable to CSRF attacks—for example, endpoints that aren't callable from a browser or that are secured with non-cookie authentication such as bearer tokens or API keys. Don't disable CSRF protection on browser-accessible endpoints that rely on cookies for authentication. + +### Disabling globally + +The middleware can be disabled across the entire app using the `DisableCsrfProtection` configuration key. This is an escape hatch—prefer per-endpoint opt-outs. + +In `appsettings.json`: + +```json +{ + "DisableCsrfProtection": true +} +``` + +Or as an environment variable: + +```text +ASPNETCORE_DisableCsrfProtection=true +``` + +When this key is set to `true`, `WebApplication` skips registering the middleware in the pipeline. The `ICsrfProtection` service remains registered, so anything that resolves it directly continues to work. + +> [!WARNING] +> The automatic CSRF middleware also satisfies the antiforgery requirement for endpoints that require validation, even when an app doesn't call `app.UseAntiforgery()`. If an app relies on antiforgery but doesn't call `app.UseAntiforgery()`, disabling the CSRF middleware globally or running on a host that isn't built with `WebApplication`, where the middleware isn't injected, leaves those endpoints with no antiforgery middleware. A request to such an endpoint then throws an exception. Call `app.UseAntiforgery()` in that configuration. + +### Browser support + +`Sec-Fetch-Site` is supported by all current versions of Chromium-based browsers, Firefox, and Safari. For an authoritative compatibility table, see the [MDN reference for `Sec-Fetch-Site`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Sec-Fetch-Site). + +Older browsers that predate Fetch Metadata don't send `Sec-Fetch-Site`. For those clients, the middleware falls back to comparing the `Origin` header against the request's scheme and host. Browsers have sent `Origin` on cross-origin write requests for many years, so this fallback covers essentially all legacy browser traffic. + +Non-browser clients—`curl`, Postman, mobile apps, server-to-server callers—typically send neither `Sec-Fetch-Site` nor `Origin`. Those requests are allowed because CSRF is a browser-only attack vector that depends on the browser automatically attaching ambient credentials such as cookies. A non-browser client that wants to attack the API has no need for CSRF; it can simply call the API directly with whatever credentials it possesses. + +### Customizing: implement `ICsrfProtection` + +The decision logic lives behind a one-method interface: + +```csharp +namespace Microsoft.AspNetCore.Antiforgery; + +public interface ICsrfProtection +{ + ValueTask ValidateAsync(HttpContext context); +} +``` + +To replace the default implementation, register a singleton in DI. Because the framework uses `TryAddSingleton`, an explicit `AddSingleton` call overrides the default: + +```csharp +builder.Services.AddSingleton(); +``` + +A custom implementation is useful when the trust model doesn't fit CORS—for example, when a fixed allowlist of partner origins is preferred, or when stricter rules are needed: + +```csharp +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; + +public sealed class AllowlistCsrfProtection : ICsrfProtection +{ + private static readonly HashSet SafeMethods = + new(StringComparer.OrdinalIgnoreCase) { "GET", "HEAD", "OPTIONS", "TRACE" }; + + private static readonly HashSet TrustedOrigins = + new(StringComparer.OrdinalIgnoreCase) + { + "https://app.contoso.com", + "https://admin.contoso.com", + }; + + public ValueTask ValidateAsync(HttpContext context) + { + if (SafeMethods.Contains(context.Request.Method)) + { + return ValueTask.FromResult(CsrfProtectionResult.Allowed()); + } + + var origin = context.Request.Headers.Origin.ToString(); + var allowed = !string.IsNullOrEmpty(origin) && TrustedOrigins.Contains(origin); + + return ValueTask.FromResult( + allowed ? CsrfProtectionResult.Allowed() : CsrfProtectionResult.Denied()); + } +} +``` + +The middleware still honors `.DisableAntiforgery()` / `[IgnoreAntiforgeryToken]` regardless of which implementation is registered—opt-out is handled by the middleware itself before `ValidateAsync` is called. + +### Interaction with token-based antiforgery + +The two CSRF defenses target different layers and are designed to coexist. They also share the same request feature: both record their result on , and form consumers enforce whatever verdict is present. + +| Aspect | Token-based `AntiforgeryMiddleware` | Automatic CSRF protection middleware | +|---|---|---| +| Introduced | ASP.NET Core 2.0+ | .NET 11 | +| Activation | Opt-in via `app.UseAntiforgery()` (or implicitly by `AddMvc` / `MapRazorPages` / `AddRazorComponents`) | Auto-injected by `WebApplication.CreateBuilder` | +| Validates | Synchronized token (form field + cookie pair) | `Sec-Fetch-Site` / `Origin` headers | +| Requires | for token encryption | No tokens, no state | +| Browser scope | All browsers that send cookies | All modern browsers; `Origin` fallback for legacy | +| Per-endpoint opt-out | `.DisableAntiforgery()` / `[IgnoreAntiforgeryToken]` | Same — both honor the same metadata | + +The token-based middleware specifically protects against the classic CSRF attack pattern in which a malicious site triggers a form `POST` to a vulnerable site using the user's ambient cookies. The automatic CSRF middleware addresses the same threat at the HTTP layer using browser-supplied metadata. Both can be active on the same endpoint, and many apps will benefit from defense in depth: + +* **Razor Pages, MVC, and Blazor SSR apps** that already use the token system gain a header-based check that runs before token validation, without changing the token flow. +* **Minimal API apps** that bind forms get a useful default without needing to call `app.UseAntiforgery()` or thread `IAntiforgery` through endpoints. +* **APIs called from cross-origin SPAs** can rely on this middleware combined with a CORS allowlist, and skip the token system entirely if the API never serves HTML forms. + +The automatic CSRF middleware replaces the token-based system in many scenarios, because both protect the same form-handling endpoints. Keep the token-based system when: + +* The app must support browsers that don't send `Sec-Fetch-Site`. See [Browser support](#browser-support). +* The app uses to round-trip extra data inside the token. +* A security review or compliance requirement specifies the token defense as an independent layer. + +For details on the token-based system, including form integration, AJAX flows, configuration via `AntiforgeryOptions`, and `IAntiforgery` APIs, see [Antiforgery in ASP.NET Core](#antiforgery-in-aspnet-core). + +#### Token validation takes precedence + +When an app calls `app.UseAntiforgery()`, the token-based middleware runs after the automatic CSRF middleware. The token middleware clears any verdict the CSRF middleware recorded and replaces it with the result of token validation. The token result is authoritative: + +* A request that the CSRF middleware marked invalid becomes valid if it carries a valid token. +* A request that the CSRF middleware allowed is marked invalid if its token is missing or invalid. + +This ordering means apps that use the token system see the same end-to-end behavior they had before the automatic middleware existed, while apps that don't use tokens fall back to the CSRF middleware's verdict. + +### Blazor static server-side rendering + +Blazor static server-side rendering (SSR) endpoints participate in the same deferred model. The Razor Components endpoint trusts the verdict recorded on by the upstream middleware and returns `400 - Bad Request` for a form post only when that verdict is invalid. The endpoint no longer validates the request itself. + +The behavior depends on which middleware ran: + +* Apps that call `app.UseAntiforgery()` are unchanged. The token-based middleware validates each request, and antiforgery tokens are generated for rendered forms as before. +* Apps that don't call `app.UseAntiforgery()` are protected by the automatic CSRF middleware instead. In that configuration, the endpoint skips antiforgery token generation because no token middleware is present to validate a token on a later request. + +This is a behavior change for static SSR that previously removed `app.UseAntiforgery()`: they're now protected by the CSRF middleware rather than left unprotected, and they stop emitting antiforgery tokens. For migration guidance, see . For the formal breaking-change notice, see [Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection). + +### Troubleshooting + +**Symptom:** Same-origin requests from a browser succeed, but cross-origin form posts return `400 - Bad Request` with no body. + +**Cause:** The CSRF middleware recorded an invalid verdict for the cross-origin request, and a form-processing component—such as an MVC action, a Minimal API form binding, or a Blazor SSR form post—enforced that verdict with a `400 - Bad Request`. This is the expected default behavior for endpoints that process forms. + +**Resolution:** Choose one of the following, depending on the scenario: + +* If the calling origin is known and trusted, [allow it via CORS](#allowing-cross-origin-clients). +* If the endpoint isn't browser-reachable or uses non-cookie authentication, [opt it out](#opting-an-endpoint-out) with `.DisableAntiforgery()` or `[IgnoreAntiforgeryToken]`. +* If the entire app needs to opt out (for example, during a migration window), [disable globally](#disabling-globally). + +**Diagnosing:** The middleware logs every invalid verdict at `Debug` level under the category `Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware` with event name `CsrfValidationFailed`. Enable `Debug` logging for that category in `appsettings.Development.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware": "Debug" + } + } +} +``` + +A recorded verdict then appears in the log as: + +```text +dbug: Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware[1] + Cross-origin CSRF protection marked request POST /widgets from origin 'https://attacker.example.com' as invalid. +``` + +**Reproducing locally:** Use `curl` with an explicit `Origin` header to simulate a cross-origin browser request against a form endpoint: + +```bash +curl -i -X POST https://localhost:{PORT}/widgets \ + -H "Origin: https://attacker.example.com" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "name=test" +``` + +Replace `{PORT}` with the app's local HTTPS port. The `400 - Bad Request` is observed because the endpoint binds the form, which enforces the recorded verdict. A non-form endpoint returns its normal response because nothing reads the verdict. Without the `Origin` header, the same request is allowed because `curl` doesn't send `Sec-Fetch-Site` either and a request with neither header is treated as a non-browser client. + +The token-based antiforgery system described in the rest of this article predates this middleware and remains available. For most apps, the automatic protection is enough on its own. For guidance on when to keep the token-based system and how to migrate, see . + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0" + ## Antiforgery in ASP.NET Core diff --git a/aspnetcore/security/csrf-protection.md b/aspnetcore/security/csrf-protection.md deleted file mode 100644 index e55fc86ee489..000000000000 --- a/aspnetcore/security/csrf-protection.md +++ /dev/null @@ -1,335 +0,0 @@ ---- -title: Automatic CSRF protection in ASP.NET Core -ai-usage: ai-assisted -author: tdykstra -description: Learn how the automatic Cross-Site Request Forgery (CSRF) protection middleware in .NET 11 uses Fetch Metadata and Origin validation to reject cross-origin form posts by default. -monikerRange: '>= aspnetcore-11.0' -ms.author: tdykstra -ms.custom: mvc -ms.date: 06/25/2026 -uid: security/csrf-protection ---- -# Automatic CSRF protection in ASP.NET Core - -Starting in .NET 11, ASP.NET Core ships an automatic Cross-Site Request Forgery (CSRF) protection middleware that's enabled by default in apps built with `WebApplication.CreateBuilder`. Unlike the [token-based antiforgery system](xref:security/anti-request-forgery), this middleware doesn't issue or validate tokens. Instead, it inspects the [Fetch Metadata](https://developer.mozilla.org/docs/Web/HTTP/Headers/Sec-Fetch-Site) headers that modern browsers attach to every request, with the [`Origin`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Origin) header as a fallback, then records a validation verdict on the request. Components that process submitted form data enforce that verdict, rejecting cross-origin form posts that aren't explicitly trusted. - -For most apps, no code changes are required: same-origin browser requests, safe HTTP methods, and non-browser clients (`curl`, server-to-server, mobile apps) all pass through unaffected. The middleware primarily affects apps that accept cross-origin form posts from a browser, such as a site that posts a form to an API on a different origin. Those scenarios need to either configure [CORS](xref:security/cors) to declare the trusted origin or opt the endpoint out. - -This middleware is *additive* to the token-based antiforgery system. The two protections coexist and can both be active on the same endpoint. For a comparison of when each one applies, see [Interaction with token-based antiforgery](#interaction-with-token-based-antiforgery) later in this article. - -## How it works - -For every request, the middleware evaluates a short chain of rules to reach a verdict — *allowed* or *denied*. The checks run in order, and the first match wins: - -1. **Safe HTTP methods are always allowed.** `GET`, `HEAD`, `OPTIONS`, and `TRACE` requests pass through. This follows [RFC 9110 §9.2.1](https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.1) and is consistent with the long-standing rule that endpoints shouldn't change state on `GET`. -1. **`Sec-Fetch-Site: same-origin` or `Sec-Fetch-Site: none` is allowed.** Modern browsers send `Sec-Fetch-Site` on every request. `same-origin` covers normal in-app navigation and fetch, and `none` covers requests initiated directly by the user (typing a URL, using a bookmark). This is the most common code path — most legitimate browser traffic exits here. -1. **A trusted origin from CORS is allowed.** If the request carries an `Origin` header and the endpoint's resolved CORS policy trusts that origin, the request is allowed. The middleware resolves the policy the same way the CORS middleware does: per-endpoint policy from `[EnableCors("name")]` first, then the default policy registered with `AddDefaultPolicy`. See [Allowing cross-origin clients](#allowing-cross-origin-clients) for important limits on this rule. -1. **Any other `Sec-Fetch-Site` value is denied.** When `Sec-Fetch-Site` is `cross-site` or `same-site` and the origin isn't trusted via CORS, the request is denied. -1. **No `Sec-Fetch-Site`, but `Origin` is present:** the middleware compares the `Origin` to `scheme://host[:port]` built from the request. If they match, the request is allowed; otherwise it's denied. This is the fallback path for browsers older than the Fetch Metadata spec (released ~2020). -1. **No `Sec-Fetch-Site` and no `Origin`: the request is allowed.** Browsers always send at least one of these on a write request, so a request missing both is almost certainly a non-browser client such as `curl`, Postman, a mobile app, or a server-to-server caller. CSRF is a browser-only attack vector, so these requests pass through. - -The middleware records this verdict on the request rather than ending the request itself. For how and when a denied verdict turns into an HTTP `400 Bad Request` response, see [Deferred validation](#deferred-validation). - -### Why `Sec-Fetch-Site` and `Origin` can be trusted - -Both `Sec-Fetch-Site` and `Origin` are [forbidden request headers](https://developer.mozilla.org/docs/Glossary/Forbidden_request_header): they're set by the browser and JavaScript running in the page can't override or forge them. A malicious page can't disguise a cross-origin request as same-origin by attaching its own header value — the browser strips or rejects the attempt. That's what makes Fetch Metadata a reliable CSRF signal without requiring a server-issued token. - -## Deferred validation - -The middleware doesn't reject a request on its own. Instead, it records its verdict on the request's `IAntiforgeryValidationFeature` — the same feature the token-based antiforgery system uses — where a denied verdict is recorded as *invalid*. The request continues down the pipeline; an invalid verdict becomes an HTTP `400 Bad Request` only when a component that processes form data observes it. This deferral matches how the token-based system already behaves: the verdict is produced early but enforced at the point where a form is consumed. - -The following components read `IAntiforgeryValidationFeature` and reject a request with `400` when the recorded verdict is invalid: - -* MVC actions protected by antiforgery. -* Minimal API endpoints that bind a form parameter. -* Blazor static server-side rendering (SSR) form posts. -* Any code that reads the request form directly, which acts as a backstop. - -Each consumer first confirms that an antiforgery or CSRF middleware actually ran before it trusts the verdict, so a pipeline without either middleware doesn't produce false rejections. - -A consequence of this model is that an endpoint that never reads form data runs even when the verdict is invalid. For example, a JSON API endpoint that binds its body from JSON, or a handler that ignores the request body, isn't rejected automatically on a cross-origin request. The verdict is still recorded on `IAntiforgeryValidationFeature` for code that wants to inspect it, but nothing enforces it. CSRF is a form-and-cookie attack vector, so endpoints that don't consume a browser-submitted form generally don't need this rejection. Endpoints that do process forms — Razor Pages, MVC views, Blazor SSR, and minimal API form binding — get the protection automatically. - -## Default behavior - -The middleware is registered automatically by `WebApplication.CreateBuilder` and runs after authentication and authorization. It validates each request using the registered `ICsrfProtection` implementation, which by default applies the rules described in [How it works](#how-it-works). The default implementation can be replaced; see [Customizing: implement `ICsrfProtection`](#customizing-implement-icsrfprotection). To turn the middleware off entirely, see [Disabling globally](#disabling-globally). - -The result is that a minimal app with a form-handling endpoint like the following is already protected: - -```csharp -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); - -app.MapPost("/widgets", ([FromForm] Widget w) => - Results.Created($"/widgets/{w.Id}", w)); - -app.Run(); -``` - -A browser making a same-origin `POST /widgets` request reaches the endpoint normally. A browser at `https://attacker.example.com` posting the same form is rejected with `400` when the endpoint binds the form, before the handler body runs. A `curl` request without `Sec-Fetch-Site` or `Origin` is allowed. - -Because rejection is [deferred to form consumers](#deferred-validation), an endpoint that doesn't read form data — such as a JSON API that binds its body from JSON — isn't rejected automatically, even on a cross-origin request. The verdict is still recorded on the request for code that wants to inspect it. - -The middleware integrates with the existing antiforgery model: - -* **Minimal APIs:** Calling `.DisableAntiforgery()` on an endpoint opts that endpoint out of *both* the token-based middleware and this middleware. The same metadata (`IAntiforgeryMetadata { RequiresValidation = false }`) is checked by both. -* **MVC controllers and actions:** `[IgnoreAntiforgeryToken]` is bridged to the same metadata in .NET 11, so it also opts the endpoint out of both protections. - -## Allowing cross-origin clients - -The most common scenario that requires action is a browser-based client that submits a cross-origin form — for example, a site at `https://app.contoso.com` posting a form to an API at `https://api.contoso.com`. Such form posts are denied by default because `Sec-Fetch-Site` is `same-site` or `cross-site` rather than `same-origin`, and the form consumer enforces that verdict with a `400`. - -The CSRF middleware doesn't introduce its own trust list. It reuses the same CORS policy that the [CORS middleware](xref:security/cors) resolves for the endpoint: if that policy allows the request's `Origin`, the CSRF middleware records an allowed verdict for the request. - -The policy is picked per-endpoint in this order: - -1. `[EnableCors("api")]` (MVC) or `.RequireCors("api")` (Minimal API) → the named policy `"api"`. -1. No CORS metadata on the endpoint → the default policy registered with `AddDefaultPolicy`. -1. No matching policy (named policy not registered, no default policy, or `services.AddCors()` never called) → no CORS-derived trust. The middleware falls through to the `Sec-Fetch-Site` and Origin-vs-Host rules. - -A minimal example using a default policy and a Minimal API endpoint: - -```csharp -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddCors(options => -{ - options.AddDefaultPolicy(policy => - policy.WithOrigins("https://app.contoso.com") - .AllowAnyHeader() - .AllowAnyMethod()); -}); - -var app = builder.Build(); - -app.UseCors(); - -app.MapPost("/widgets", ([FromForm] Widget w) => - Results.Created($"/widgets/{w.Id}", w)); - -app.Run(); -``` - -For a named policy on a single endpoint: - -```csharp -app.MapPost("/widgets", ([FromForm] Widget w) => Results.Created($"/widgets/{w.Id}", w)) - .RequireCors("api"); -``` - -> [!WARNING] -> `AllowAnyOrigin` is intentionally **not** honored as a CSRF trust signal. `AllowAnyOrigin` means "any browser can read this resource", which is a different concern than "any origin may mutate state on the user's behalf". Treating `AllowAnyOrigin` as trusted would turn this middleware into a no-op for cross-origin writes. Apps that need a public-read CORS policy combined with CSRF-protected writes should list trusted write origins explicitly with `WithOrigins`, or [opt write endpoints out](#opting-an-endpoint-out) if they don't rely on cookie-based authentication. - -`[DisableCors]` on an endpoint isn't a CSRF opt-out. It skips the CORS-derived trust step, and the request still has to satisfy the `Sec-Fetch-Site` and Origin-vs-Host rules. To opt out of CSRF protection, see [Opting an endpoint out](#opting-an-endpoint-out). - -For details on configuring CORS itself — `AddCors`, `AddDefaultPolicy`, `AddPolicy`, `WithOrigins`, and the rest of the policy builder API — see . - -## Opting an endpoint out - -If an endpoint isn't browser-reachable, or is secured by a non-cookie mechanism such as a bearer token or API key, opt it out individually rather than disabling the middleware globally. - -**Minimal APIs** — call `DisableAntiforgery` on the endpoint or group: - -```csharp -app.MapPost("/api/webhook", (WebhookPayload p) => Results.Accepted()) - .DisableAntiforgery(); -``` - -**MVC controllers** — apply `[IgnoreAntiforgeryToken]` to the action or controller: - -```csharp -[ApiController] -[Route("api/[controller]")] -[IgnoreAntiforgeryToken] -public class WebhookController : ControllerBase -{ - [HttpPost] - public IActionResult Post([FromBody] WebhookPayload payload) => Accepted(); -} -``` - -Either approach adds `IAntiforgeryMetadata { RequiresValidation = false }` to the endpoint, which the CSRF middleware honors by skipping validation. - -> [!WARNING] -> Disabling CSRF protection on an endpoint should only be done when the endpoint isn't vulnerable to CSRF attacks — for example, endpoints that aren't callable from a browser, or that are secured with non-cookie authentication such as bearer tokens or API keys. Don't disable CSRF protection on browser-accessible endpoints that rely on cookies for authentication. - -## Disabling globally - -The middleware can be disabled across the entire app using the `DisableCsrfProtection` configuration key. This is an escape hatch — prefer per-endpoint opt-outs. - -In `appsettings.json`: - -```json -{ - "DisableCsrfProtection": true -} -``` - -Or as an environment variable: - -```text -ASPNETCORE_DisableCsrfProtection=true -``` - -When this key is set to `true`, `WebApplication` skips registering the middleware in the pipeline. The `ICsrfProtection` service remains registered, so anything that resolves it directly continues to work. - -> [!WARNING] -> The automatic CSRF middleware also satisfies the antiforgery requirement for endpoints that require validation, even when an app doesn't call `app.UseAntiforgery()`. If an app relies on antiforgery but doesn't call `app.UseAntiforgery()`, disabling the CSRF middleware globally — or running on a host that isn't built with `WebApplication`, where the middleware isn't injected — leaves those endpoints with no antiforgery middleware. A request to such an endpoint then throws an exception. Call `app.UseAntiforgery()` in that configuration. - -## Browser support - -`Sec-Fetch-Site` is supported by all current versions of Chromium-based browsers, Firefox, and Safari. For an authoritative compatibility table, see the [MDN reference for `Sec-Fetch-Site`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Sec-Fetch-Site). - -Older browsers that predate Fetch Metadata don't send `Sec-Fetch-Site`. For those clients, the middleware falls back to comparing the `Origin` header against the request's scheme and host. Browsers have sent `Origin` on cross-origin write requests for many years, so this fallback covers essentially all legacy browser traffic. - -Non-browser clients — `curl`, Postman, mobile apps, server-to-server callers — typically send neither `Sec-Fetch-Site` nor `Origin`. Those requests are allowed because CSRF is a browser-only attack vector that depends on the browser automatically attaching ambient credentials such as cookies. A non-browser client that wants to attack the API has no need for CSRF; it can simply call the API directly with whatever credentials it possesses. - -## Customizing: implement `ICsrfProtection` - -The decision logic lives behind a one-method interface: - -```csharp -namespace Microsoft.AspNetCore.Antiforgery; - -public interface ICsrfProtection -{ - ValueTask ValidateAsync(HttpContext context); -} -``` - -To replace the default implementation, register a singleton in DI. Because the framework uses `TryAddSingleton`, an explicit `AddSingleton` call overrides the default: - -```csharp -builder.Services.AddSingleton(); -``` - -A custom implementation is useful when the trust model doesn't fit CORS — for example, when a fixed allowlist of partner origins is preferred, or when stricter rules are needed: - -```csharp -using Microsoft.AspNetCore.Antiforgery; -using Microsoft.AspNetCore.Http; - -public sealed class AllowlistCsrfProtection : ICsrfProtection -{ - private static readonly HashSet SafeMethods = - new(StringComparer.OrdinalIgnoreCase) { "GET", "HEAD", "OPTIONS", "TRACE" }; - - private static readonly HashSet TrustedOrigins = - new(StringComparer.OrdinalIgnoreCase) - { - "https://app.contoso.com", - "https://admin.contoso.com", - }; - - public ValueTask ValidateAsync(HttpContext context) - { - if (SafeMethods.Contains(context.Request.Method)) - { - return ValueTask.FromResult(CsrfProtectionResult.Allowed()); - } - - var origin = context.Request.Headers.Origin.ToString(); - var allowed = !string.IsNullOrEmpty(origin) && TrustedOrigins.Contains(origin); - - return ValueTask.FromResult( - allowed ? CsrfProtectionResult.Allowed() : CsrfProtectionResult.Denied()); - } -} -``` - -The middleware still honors `.DisableAntiforgery()` / `[IgnoreAntiforgeryToken]` regardless of which implementation is registered — opt-out is handled by the middleware itself before `ValidateAsync` is called. - -## Interaction with token-based antiforgery - -The two CSRF defenses target different layers and are designed to coexist. They also share the same request feature: both record their result on `IAntiforgeryValidationFeature`, and form consumers enforce whatever verdict is present. - -| Aspect | Token-based `AntiforgeryMiddleware` | Automatic CSRF protection middleware | -|---|---|---| -| Introduced | ASP.NET Core 2.0+ | .NET 11 | -| Activation | Opt-in via `app.UseAntiforgery()` (or implicitly by `AddMvc` / `MapRazorPages` / `AddRazorComponents`) | Auto-injected by `WebApplication.CreateBuilder` | -| Validates | Synchronized token (form field + cookie pair) | `Sec-Fetch-Site` / `Origin` headers | -| Requires | for token encryption | No tokens, no state | -| Browser scope | All browsers that send cookies | All modern browsers; `Origin` fallback for legacy | -| Per-endpoint opt-out | `.DisableAntiforgery()` / `[IgnoreAntiforgeryToken]` | Same — both honor the same metadata | - -The token-based middleware specifically protects against the classic CSRF attack pattern in which a malicious site triggers a form `POST` to a vulnerable site using the user's ambient cookies. The automatic CSRF middleware addresses the same threat at the HTTP layer using browser-supplied metadata. Both can be active on the same endpoint, and many apps will benefit from defense in depth: - -* **Razor Pages, MVC, and Blazor SSR apps** that already use the token system gain a header-based check that runs before token validation, without changing the token flow. -* **Minimal API apps** that bind forms get a useful default without needing to call `app.UseAntiforgery()` or thread `IAntiforgery` through endpoints. -* **APIs called from cross-origin SPAs** can rely on this middleware combined with a CORS allowlist, and skip the token system entirely if the API never serves HTML forms. - -For details on the token-based system — including form integration, AJAX flows, configuration via `AntiforgeryOptions`, and `IAntiforgery` APIs — see . - -### Token validation takes precedence - -When an app calls `app.UseAntiforgery()`, the token-based middleware runs after the automatic CSRF middleware. The token middleware clears any verdict the CSRF middleware recorded and replaces it with the result of token validation. The token result is authoritative: - -* A request that the CSRF middleware marked invalid becomes valid if it carries a valid token. -* A request that the CSRF middleware allowed is marked invalid if its token is missing or invalid. - -This ordering means apps that use the token system see the same end-to-end behavior they had before the automatic middleware existed, while apps that don't use tokens fall back to the CSRF middleware's verdict. - -## Blazor server-side rendering - -Blazor static server-side rendering (SSR) endpoints participate in the same deferred model. The Razor Components endpoint trusts the verdict recorded on `IAntiforgeryValidationFeature` by the upstream middleware and returns `400 Bad Request` for a form post only when that verdict is invalid. The endpoint no longer validates the request itself. - -The behavior depends on which middleware ran: - -* Apps that call `app.UseAntiforgery()` are unchanged. The token-based middleware validates each request, and antiforgery tokens are generated for rendered forms as before. -* Apps that don't call `app.UseAntiforgery()` are protected by the automatic CSRF middleware instead. In that configuration, the endpoint skips antiforgery token generation, because no token middleware is present to validate a token on a later request. - -This is a behavior change for Blazor SSR apps that previously removed `app.UseAntiforgery()`: they're now protected by the CSRF middleware rather than left unprotected, and they stop emitting antiforgery tokens. For migration guidance, see . For the formal breaking-change notice, see [Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection). - -## Adopting CSRF-only protection in existing apps - -For apps that are upgrading from .NET 10 and already use the token-based system, the automatic CSRF middleware can replace it entirely in many scenarios. For guidance on when to drop the token-based system and how to do so, see . - -## Troubleshooting - -**Symptom:** Same-origin requests from a browser succeed, but cross-origin form posts return `400` with no body. - -**Cause:** The CSRF middleware recorded an invalid verdict for the cross-origin request, and a form-processing component — such as an MVC action, a Minimal API form binding, or a Blazor SSR form post — enforced that verdict with a `400`. This is the expected default behavior for endpoints that process forms. - -**Resolution:** Choose one of the following, depending on the scenario: - -* If the calling origin is known and trusted, [allow it via CORS](#allowing-cross-origin-clients). -* If the endpoint isn't browser-reachable or uses non-cookie authentication, [opt it out](#opting-an-endpoint-out) with `.DisableAntiforgery()` or `[IgnoreAntiforgeryToken]`. -* If the entire app needs to opt out (for example, during a migration window), [disable globally](#disabling-globally). - -**Diagnosing:** The middleware logs every invalid verdict at `Debug` level under the category `Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware` with event name `CsrfValidationFailed`. Enable `Debug` logging for that category in `appsettings.Development.json`: - -```json -{ - "Logging": { - "LogLevel": { - "Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware": "Debug" - } - } -} -``` - -A recorded verdict then appears in the log as: - -```text -dbug: Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware[1] - Cross-origin CSRF protection marked request POST /widgets from origin 'https://attacker.example.com' as invalid. -``` - -**Reproducing locally:** Use `curl` with an explicit `Origin` header to simulate a cross-origin browser request against a form endpoint: - -```bash -curl -i -X POST https://localhost:{PORT}/widgets \ - -H "Origin: https://attacker.example.com" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "name=test" -``` - -Replace `{PORT}` with the app's local HTTPS port. The `400` is observed because the endpoint binds the form, which enforces the recorded verdict. A non-form endpoint returns its normal response, because nothing reads the verdict. Without the `Origin` header, the same request is allowed because `curl` doesn't send `Sec-Fetch-Site` either, and a request with neither header is treated as a non-browser client. - -## Additional resources - -* — the token-based antiforgery system, including form integration and `IAntiforgery` APIs. -* — when and how to drop the token-based system in favor of the automatic middleware. -* — configuring CORS policies, which this middleware uses as its trusted-origin source. -* [Breaking change: Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection) -* [MDN — `Sec-Fetch-Site`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Sec-Fetch-Site) -* [MDN — `Origin`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Origin) diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 87e6b55d2b4c..f74171a6e62b 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -2238,8 +2238,6 @@ items: uid: security/gdpr - name: Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks uid: security/anti-request-forgery - - name: Automatic CSRF protection - uid: security/csrf-protection - name: Prevent open redirect attacks uid: security/preventing-open-redirects - name: Prevent Cross-Site Scripting (XSS) @@ -2386,9 +2384,6 @@ items: - name: Overview displayName: migrate, migration uid: migration/index - - name: Adopt automatic CSRF protection in .NET 11 - displayName: migrate, migration, csrf, antiforgery - uid: migration/antiforgery-to-csrf - name: Version updates items: - name: 10 to 11