From f805ab8a0046c4058932f35cf25099d53be6864e Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 10 Jul 2026 09:52:17 +0200 Subject: [PATCH 01/11] Mention automatic CSRF protection in .NET 10 -> 11 migration article Adds a short "Security" section pointing readers at and . Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aspnetcore/migration/100-to-110.md | 6 +++++- aspnetcore/migration/100-to-110/includes/security.md | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 aspnetcore/migration/100-to-110/includes/security.md diff --git a/aspnetcore/migration/100-to-110.md b/aspnetcore/migration/100-to-110.md index b3b84cc3a065..1dfc5a4b9b7a 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/10/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..d1a05f89d110 --- /dev/null +++ b/aspnetcore/migration/100-to-110/includes/security.md @@ -0,0 +1,10 @@ +### Adopt automatic CSRF protection + +.NET 11 adds an automatic Cross-Site Request Forgery (CSRF) protection middleware that's enabled by default in apps built with `WebApplication.CreateBuilder`. The middleware inspects the `Sec-Fetch-Site` and `Origin` headers on unsafe HTTP methods and records a validation verdict on the request. Form-consuming components — MVC actions, minimal API form binding, and Blazor server-side rendering (SSR) form posts — enforce that verdict and return `400 Bad Request` for cross-origin form posts that aren't trusted. + +Most apps aren't affected: same-origin browser traffic, safe HTTP methods, and non-browser clients such as `curl`, mobile apps, and server-to-server callers pass through unchanged. Apps that accept **cross-origin form posts from a browser** — for example, a site at `https://app.contoso.com` posting a form to an API at `https://api.contoso.com` — need to either: + +* Configure [CORS](xref:security/cors) so the API's resolved policy trusts the caller's origin, or +* Opt the endpoint out by calling `.DisableAntiforgery()` on the endpoint. + +For a full description of the middleware, how the verdict is computed, and how it interacts with the token-based antiforgery system, see . Apps that use the token-based antiforgery system today can also review for guidance on whether to keep or drop token validation. From 30dcbdcbad24ccb1d4780ffbe9ef80ccc2dabdd8 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 10 Jul 2026 09:59:39 +0200 Subject: [PATCH 02/11] Restructure security include by usage scenario Splits the guidance into "already uses AddAntiforgery/UseAntiforgery" vs "doesn't configure antiforgery explicitly", then covers opt-out and CORS in a single follow-up paragraph. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aspnetcore/migration/100-to-110/includes/security.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/aspnetcore/migration/100-to-110/includes/security.md b/aspnetcore/migration/100-to-110/includes/security.md index d1a05f89d110..97c741f160ed 100644 --- a/aspnetcore/migration/100-to-110/includes/security.md +++ b/aspnetcore/migration/100-to-110/includes/security.md @@ -2,9 +2,12 @@ .NET 11 adds an automatic Cross-Site Request Forgery (CSRF) protection middleware that's enabled by default in apps built with `WebApplication.CreateBuilder`. The middleware inspects the `Sec-Fetch-Site` and `Origin` headers on unsafe HTTP methods and records a validation verdict on the request. Form-consuming components — MVC actions, minimal API form binding, and Blazor server-side rendering (SSR) form posts — enforce that verdict and return `400 Bad Request` for cross-origin form posts that aren't trusted. -Most apps aren't affected: same-origin browser traffic, safe HTTP methods, and non-browser clients such as `curl`, mobile apps, and server-to-server callers pass through unchanged. Apps that accept **cross-origin form posts from a browser** — for example, a site at `https://app.contoso.com` posting a form to an API at `https://api.contoso.com` — need to either: +Depending on how your app uses antiforgery today: -* Configure [CORS](xref:security/cors) so the API's resolved policy trusts the caller's origin, or -* Opt the endpoint out by calling `.DisableAntiforgery()` on the endpoint. +* **If the app calls `AddAntiforgery()` and `UseAntiforgery()` explicitly:** consider dropping both calls and relying on the automatic CSRF protection instead. For most apps this is a one-line change and doesn't require any other code updates. The token-based system remains available if you still need it — see for the trade-offs. +* **If the app doesn't configure antiforgery explicitly:** you now get CSRF protection automatically. Same-origin browser traffic, safe HTTP methods, and non-browser clients such as `curl`, mobile apps, and server-to-server callers pass through unchanged. No action is required. + +To opt an endpoint out, call `.DisableAntiforgery()` on it. To allow a specific cross-origin browser client, configure [CORS](xref:security/cors) so the endpoint's resolved policy trusts the caller's origin; the CSRF middleware honors that policy. + +For a full description of the middleware, the validation rules, and how it interacts with the token-based antiforgery system, see . -For a full description of the middleware, how the verdict is computed, and how it interacts with the token-based antiforgery system, see . Apps that use the token-based antiforgery system today can also review for guidance on whether to keep or drop token validation. From 04927a57e97e025622fc9c803971f63c541081c6 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 15 Jul 2026 16:11:23 +0200 Subject: [PATCH 03/11] Consolidate CSRF migration guidance and update reference to latest implementation - Delete standalone migration/antiforgery-to-csrf.md; add redirect and remove toc entry - Inline a short CSRF migration section into the 100-to-110 security include - Update security/csrf-protection.md to match the shipped middleware: scope validation to endpoints with IAntiforgeryMetadata.RequiresValidation, record a verdict instead of short-circuiting, correct registration conditions, fix examples to form-handling endpoints, and update the log event name/message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .openpublishing.redirection.json | 5 + .../migration/100-to-110/includes/security.md | 23 +- aspnetcore/migration/antiforgery-to-csrf.md | 214 ------------------ aspnetcore/security/csrf-protection.md | 109 ++++++--- aspnetcore/toc.yml | 3 - 5 files changed, 97 insertions(+), 257 deletions(-) delete mode 100644 aspnetcore/migration/antiforgery-to-csrf.md diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 5dd4f1a65a8d..4739e5ed748e 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -570,6 +570,11 @@ "redirect_url": "/aspnet/core", "redirect_document_id": false }, + { + "source_path": "aspnetcore/migration/antiforgery-to-csrf.md", + "redirect_url": "/aspnet/core/security/csrf-protection", + "redirect_document_id": false + }, { "source_path": "aspnetcore/migration/rc1-to-rtm.md", "redirect_url": "/aspnet/core/migration/", diff --git a/aspnetcore/migration/100-to-110/includes/security.md b/aspnetcore/migration/100-to-110/includes/security.md index 97c741f160ed..84b8500b0a3a 100644 --- a/aspnetcore/migration/100-to-110/includes/security.md +++ b/aspnetcore/migration/100-to-110/includes/security.md @@ -1,13 +1,22 @@ -### Adopt automatic CSRF protection +### Automatic CSRF protection -.NET 11 adds an automatic Cross-Site Request Forgery (CSRF) protection middleware that's enabled by default in apps built with `WebApplication.CreateBuilder`. The middleware inspects the `Sec-Fetch-Site` and `Origin` headers on unsafe HTTP methods and records a validation verdict on the request. Form-consuming components — MVC actions, minimal API form binding, and Blazor server-side rendering (SSR) form posts — enforce that verdict and return `400 Bad Request` for cross-origin form posts that aren't trusted. +.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 on unsafe HTTP methods and records a validation verdict on the request. -Depending on how your app uses antiforgery today: +The middleware only validates endpoints that already require antiforgery validation, which the framework attaches automatically to form-handling endpoints: -* **If the app calls `AddAntiforgery()` and `UseAntiforgery()` explicitly:** consider dropping both calls and relying on the automatic CSRF protection instead. For most apps this is a one-line change and doesn't require any other code updates. The token-based system remains available if you still need it — see for the trade-offs. -* **If the app doesn't configure antiforgery explicitly:** you now get CSRF protection automatically. Same-origin browser traffic, safe HTTP methods, and non-browser clients such as `curl`, mobile apps, and server-to-server callers pass through unchanged. No action is required. +* Blazor server-side rendering (SSR) form posts. +* Minimal API endpoints that bind form data. +* MVC actions annotated with `[ValidateAntiForgeryToken]` or `[AutoValidateAntiforgeryToken]`. -To opt an endpoint out, call `.DisableAntiforgery()` on it. To allow a specific cross-origin browser client, configure [CORS](xref:security/cors) so the endpoint's resolved policy trusts the caller's origin; the CSRF middleware honors that policy. +Endpoints without antiforgery metadata — for example, a plain `MapPost` or `[HttpPost]` that binds JSON — aren't affected, so nothing that worked on .NET 10 starts failing on .NET 11. -For a full description of the middleware, the validation rules, and how it interacts with the token-based antiforgery system, see . +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 `IAntiforgeryAdditionalDataProvider`, 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 `AddAntiforgery()` and `UseAntiforgery()` calls and rely on the automatic protection. For most apps this is a one-line change with no other code updates. + +If a cross-origin form post from a browser starts returning `400 Bad Request` after the upgrade: +* Configure [CORS](xref:security/cors) so the endpoint's resolved policy trusts the caller's origin. The CSRF middleware honors that policy. +* Or call `.DisableAntiforgery()` (minimal APIs) or apply `[IgnoreAntiforgeryToken]` (MVC) on endpoints that aren't reachable from a browser or that use non-cookie authentication. + +For a full description of the middleware, the validation rules, and how it interacts with the token-based antiforgery system, see . diff --git a/aspnetcore/migration/antiforgery-to-csrf.md b/aspnetcore/migration/antiforgery-to-csrf.md deleted file mode 100644 index 8443cebbc8ec..000000000000 --- a/aspnetcore/migration/antiforgery-to-csrf.md +++ /dev/null @@ -1,214 +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/05/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. - -### 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. - -## 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 write requests that aren't covered by a CORS policy are rejected with HTTP `400`. - -### Symptoms - -Cross-origin browser write requests that succeeded on .NET 10 fail on .NET 11 with HTTP `400 Bad Request` and an empty response body. The endpoint code doesn't run. - -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 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/json" \ - -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 denied request appears as: - -```text -dbug: Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware[1] - Cross-origin CSRF protection denied request POST /widgets from origin 'https://app.contoso.com'. -``` - -### 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. - -## Related - -* — full reference for the new middleware. -* — the token-based antiforgery system. -* — CORS configuration reference. -* — overall .NET 10 → .NET 11 migration guide. diff --git a/aspnetcore/security/csrf-protection.md b/aspnetcore/security/csrf-protection.md index 67585ab962bb..2311ea4c0ea8 100644 --- a/aspnetcore/security/csrf-protection.md +++ b/aspnetcore/security/csrf-protection.md @@ -2,33 +2,53 @@ 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 block cross-origin write requests by default. +description: Learn how the automatic Cross-Site Request Forgery (CSRF) protection middleware in .NET 11 uses Fetch Metadata and Origin validation to protect form-handling endpoints from untrusted cross-origin requests. monikerRange: '>= aspnetcore-11.0' ms.author: tdykstra ms.custom: mvc -ms.date: 06/05/2026 +ms.date: 07/15/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 and the [`Origin`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Origin) header as a fallback, then rejects cross-origin write requests that aren't explicitly trusted. +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, and records a validation verdict on the request. The framework's antiforgery enforcement then rejects untrusted cross-origin requests to form-handling endpoints with HTTP `400 Bad Request`. -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 write requests from a browser — for example, a Single Page App (SPA) hosted on a different origin than its API. Those scenarios need to either configure [CORS](xref:security/cors) to declare the trusted origin or opt the endpoint out. +The middleware validates only endpoints that already require antiforgery validation — the same form-handling endpoints that the token-based system protects: Blazor server-side rendering (SSR) form posts, minimal API endpoints that bind form data, and MVC actions annotated with antiforgery attributes. Endpoints without antiforgery metadata, such as a plain `MapPost` or `[HttpPost]` that binds JSON, pass through unchanged, so nothing that worked on .NET 10 starts failing on .NET 11. + +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 form-handling endpoints that accept cross-origin requests from a browser — for example, a form posted from a Single Page App (SPA) hosted on a different origin than its API. 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 and either allows the request to continue or short-circuits the response with HTTP `400 Bad Request`. The checks run in order, and the first match wins: +### Which endpoints are validated + +The middleware runs after routing and inspects the matched endpoint. Validation runs only when the endpoint carries antiforgery metadata that requires validation — that is, `IAntiforgeryMetadata` with `RequiresValidation` set to `true`. The framework attaches this metadata automatically to form-handling endpoints: + +* Blazor SSR form posts. +* Minimal API endpoints that bind form data, such as an `IFormCollection`, `IFormFile`, or `[FromForm]` parameter. +* MVC actions annotated with `[ValidateAntiForgeryToken]` or `[AutoValidateAntiforgeryToken]`. + +The request passes through without validation when: + +* No endpoint was matched (routing didn't select an endpoint). +* The matched endpoint has no antiforgery metadata, such as a plain `MapPost` or `[HttpPost]` that binds JSON. +* The endpoint opted out with `.DisableAntiforgery()` or `[IgnoreAntiforgeryToken]`, which sets `RequiresValidation` to `false`. + +This scoping matches the token-based antiforgery system, so upgrading from .NET 10 doesn't cause endpoints that previously worked to start returning `400`. + +### Validation rules + +When an endpoint is in scope, the middleware evaluates a short chain of rules and records either an *allowed* or an *invalid* verdict. 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 rejected. -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. **Any other `Sec-Fetch-Site` value is invalid.** When `Sec-Fetch-Site` is `cross-site` or `same-site` and the origin isn't trusted via CORS, the request is marked invalid. +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 marked invalid. 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. -Denied requests receive a [4xx client error response](https://developer.mozilla.org/docs/Web/HTTP/Status#client_error_responses), and the endpoint code doesn't run. +The middleware doesn't short-circuit the request. It records the verdict on the request, and the framework's antiforgery enforcement — minimal API form binding, the MVC antiforgery filter, or Blazor SSR — reads that verdict and returns [`400 Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status#client_error_responses) for an invalid request before the endpoint's handler runs. When the app also calls `app.UseAntiforgery()`, the token-based middleware runs afterward and can overwrite the verdict with the result of token validation. ### Why `Sec-Fetch-Site` and `Origin` can be trusted @@ -36,29 +56,39 @@ Both `Sec-Fetch-Site` and `Origin` are [forbidden request headers](https://devel ## 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 middleware is registered automatically by `WebApplication.CreateBuilder` when all of the following are true: + +* The app has endpoints (at least one endpoint data source). Apps with no endpoints don't get the middleware. +* An `ICsrfProtection` service is registered, which it is by default. +* The `DisableCsrfProtection` configuration key isn't set to `true`. See [Disabling globally](#disabling-globally). -The result is that a minimal app like the following is already protected: +The middleware runs after routing — and after authentication and authorization — so it can observe the matched endpoint and the endpoint's resolved CORS policy. It validates each in-scope request using the registered `ICsrfProtection` implementation, which by default applies the rules described in [Validation rules](#validation-rules). 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 form-handling endpoint is protected without any additional configuration: ```csharp var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); -app.MapPost("/api/widgets", (Widget w) => Results.Created($"/api/widgets/{w.Id}", w)); +app.MapPost("/register", (IFormCollection form) => Results.Ok()); app.Run(); ``` -A browser making a same-origin `POST /api/widgets` request reaches the endpoint normally. A browser at `https://attacker.example.com` posting to the same URL is rejected with `400` before the endpoint runs. A `curl` request without `Sec-Fetch-Site` or `Origin` is allowed. +Because the endpoint binds form data, the framework marks it as requiring antiforgery validation and the middleware evaluates it. A browser making a same-origin `POST /register` request reaches the endpoint normally. A browser at `https://attacker.example.com` posting to the same URL is rejected with `400` before the handler runs. A `curl` request without `Sec-Fetch-Site` or `Origin` is allowed. -The middleware integrates with the existing antiforgery model: +By contrast, an endpoint that binds JSON has no antiforgery metadata, so it isn't in scope and the middleware doesn't validate it: -* **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. +```csharp +// Not validated by the CSRF middleware — no antiforgery metadata. +app.MapPost("/api/widgets", (Widget w) => Results.Created($"/api/widgets/{w.Id}", w)); +``` + +This matches how the token-based system works: CSRF is a concern for browser-reachable endpoints that rely on ambient cookies, which for forms is handled automatically. APIs that authenticate with a non-cookie mechanism such as a bearer token aren't vulnerable to CSRF and don't need this protection. ## Allowing cross-origin clients -The most common scenario that requires action is a browser-based client hosted on a different origin than the API — for example, a SPA at `https://app.contoso.com` calling an API at `https://api.contoso.com`. Such requests are rejected by default because `Sec-Fetch-Site` will be `same-site` or `cross-site` rather than `same-origin`. +The most common scenario that requires action is a browser-based client that submits a form to an endpoint hosted on a different origin — for example, a SPA at `https://app.contoso.com` posting a form to an API at `https://api.contoso.com`. Such requests are marked invalid by default because `Sec-Fetch-Site` will be `same-site` or `cross-site` rather than `same-origin`. 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 allows the request. @@ -85,7 +115,7 @@ var app = builder.Build(); app.UseCors(); -app.MapPost("/api/widgets", (Widget w) => Results.Created($"/api/widgets/{w.Id}", w)); +app.MapPost("/register", (IFormCollection form) => Results.Ok()); app.Run(); ``` @@ -93,7 +123,7 @@ app.Run(); For a named policy on a single endpoint: ```csharp -app.MapPost("/api/widgets", (Widget w) => Results.Created($"/api/widgets/{w.Id}", w)) +app.MapPost("/register", (IFormCollection form) => Results.Ok()) .RequireCors("api"); ``` @@ -111,7 +141,7 @@ If an endpoint isn't browser-reachable, or is secured by a non-cookie mechanism **Minimal APIs** — call `DisableAntiforgery` on the endpoint or group: ```csharp -app.MapPost("/api/webhook", (WebhookPayload p) => Results.Accepted()) +app.MapPost("/upload", (IFormCollection form) => Results.Accepted()) .DisableAntiforgery(); ``` @@ -223,7 +253,7 @@ The two CSRF defenses target different layers and are designed to coexist: | 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` | +| Activation | Opt-in via `app.UseAntiforgery()` (or implicitly by `AddMvc` / `MapRazorPages` / `AddRazorComponents`) | Auto-injected by `WebApplication.CreateBuilder`; validates form-handling endpoints | | 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 | @@ -231,21 +261,35 @@ The two CSRF defenses target different layers and are designed to coexist: 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 an additional layer that doesn't require form participation. -* **Minimal API apps** that don't use 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. +* **Razor Pages, MVC, and Blazor SSR apps** that already use the token system gain a header-based check on the same form endpoints, so they stay protected even without the token round-trip. +* **Minimal API apps** that bind form data get CSRF protection automatically, without calling `app.UseAntiforgery()` or threading `IAntiforgery` through endpoints. For details on the token-based system — including form integration, AJAX flows, configuration via `AntiforgeryOptions`, and `IAntiforgery` APIs — see . ## 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 . +Apps upgrading from .NET 10 that use the token-based antiforgery system can rely on the automatic CSRF middleware instead in many cases. Both systems protect the same form-handling endpoints, so the automatic middleware is often enough on its own. + +Keep the token-based system when: + +* The app must support browsers that don't send `Sec-Fetch-Site`. See [Browser support](#browser-support) for the baseline. +* The app uses `IAntiforgeryAdditionalDataProvider` to round-trip extra data inside the antiforgery token. +* A security review or compliance requirement specifies the token defense as an independent layer. + +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. + +To drop it, remove the `app.UseAntiforgery()` and `AddAntiforgery(...)` calls along with the token plumbing: `@Html.AntiForgeryToken()` and `asp-antiforgery` in Razor views, the `RequestVerificationToken` header in client `fetch`/AJAX code, and the `[ValidateAntiForgeryToken]` and `[AutoValidateAntiforgeryToken]` attributes. Per-endpoint opt-outs (`.DisableAntiforgery()`, `[IgnoreAntiforgeryToken]`) can stay where they are — they also opt the endpoint out of the automatic middleware. If unsure, keep both: they're complementary and there's no functional conflict between them. + +For the full upgrade walkthrough, see . ## Troubleshooting -**Symptom:** Same-origin requests from a browser succeed, but cross-origin requests return `400` with no body. +**Symptom:** Same-origin requests from a browser succeed, but cross-origin requests to a form-handling endpoint return `400` with no body. -**Cause:** The cross-origin request was rejected by the CSRF middleware. The endpoint code didn't run; this is the expected default behavior. +**Cause:** The cross-origin request was marked invalid by the CSRF middleware and rejected by the framework's antiforgery enforcement. The endpoint code didn't run; this is the expected default behavior. **Resolution:** Choose one of the following, depending on the scenario: @@ -253,7 +297,7 @@ For apps that are upgrading from .NET 10 and already use the token-based system, * 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 denial at `Debug` level under the category `Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware` with event name `CsrfRequestDenied`. Enable `Debug` logging for that category in `appsettings.Development.json`: +**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 { @@ -269,16 +313,16 @@ A denial then appears in the log as: ```text dbug: Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware[1] - Cross-origin CSRF protection denied request POST /api/widgets from origin 'https://attacker.example.com'. + Cross-origin CSRF protection marked request POST /register from origin 'https://attacker.example.com' as invalid. ``` -**Reproducing locally:** Use `curl` with an explicit `Origin` header to simulate a cross-origin browser request: +**Reproducing locally:** Use `curl` with an explicit `Origin` header to simulate a cross-origin browser request to a form-handling endpoint: ```bash -curl -i -X POST https://localhost:{PORT}/api/widgets \ +curl -i -X POST https://localhost:{PORT}/register \ -H "Origin: https://attacker.example.com" \ - -H "Content-Type: application/json" \ - -d '{"name":"test"}' + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "name=test" ``` Replace `{PORT}` with the app's local HTTPS port. 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. @@ -286,7 +330,6 @@ Replace `{PORT}` with the app's local HTTPS port. Without the `Origin` header, t ## 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. * [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 98ceb9cf43bf..49607a5f0784 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -2377,9 +2377,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 From 8e355172872800daea74ab973916f267bb55dabf Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 15 Jul 2026 17:07:03 +0200 Subject: [PATCH 04/11] Refine CSRF migration include per review feedback Drop the unsafe-HTTP-methods phrasing; frame scope as endpoints that opt in to antiforgery validation (all Blazor SSR endpoints by default, minimal/MVC via IAntiforgeryMetadata); state JSON-binding endpoints have no behavioral change; and reframe the 400 guidance so opt-out is not presented as a way to suppress the check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aspnetcore/migration/100-to-110.md | 2 +- .../migration/100-to-110/includes/security.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/aspnetcore/migration/100-to-110.md b/aspnetcore/migration/100-to-110.md index 1dfc5a4b9b7a..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: 07/10/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 diff --git a/aspnetcore/migration/100-to-110/includes/security.md b/aspnetcore/migration/100-to-110/includes/security.md index 900cbbdbedcc..9af90c7f0ee9 100644 --- a/aspnetcore/migration/100-to-110/includes/security.md +++ b/aspnetcore/migration/100-to-110/includes/security.md @@ -1,22 +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 on unsafe HTTP methods and records a validation verdict on the request. +.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 only validates endpoints that already require antiforgery validation, which the framework attaches automatically to form-handling endpoints: +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: -* Blazor server-side rendering (SSR) form posts. +* 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 annotated with `[ValidateAntiForgeryToken]` or `[AutoValidateAntiforgeryToken]`. +* MVC actions that use antiforgery validation, such as those annotated with `[ValidateAntiForgeryToken]` or `[AutoValidateAntiforgeryToken]`. -Endpoints without antiforgery metadata — for example, a plain `MapPost` or `[HttpPost]` that binds JSON — aren't affected, so nothing that worked on .NET 10 starts failing on .NET 11. +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 `IAntiforgeryAdditionalDataProvider`, 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 `AddAntiforgery()` and `UseAntiforgery()` 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). -If a cross-origin form post from a browser starts returning `400 Bad Request` after the upgrade: +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 trusts the caller's origin. The CSRF middleware honors that policy. -* Or call `.DisableAntiforgery()` (minimal APIs) or apply `[IgnoreAntiforgeryToken]` (MVC) on endpoints that aren't reachable from a browser or that use non-cookie authentication. +* 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 like a bearer token. For a full description of the middleware, the validation rules, and how it interacts with the token-based antiforgery system, see . From 9c24c4b9f298241d41020bedb9ee8daf58320db8 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 15 Jul 2026 17:34:36 +0200 Subject: [PATCH 05/11] Consolidate CSRF reference into the antiforgery article Merge security/csrf-protection.md into security/anti-request-forgery.md so there is a single article. Under Authentication fundamentals, add a Fetch metadata headers subsection (>=11.0). Add an Automatic CSRF protection in ASP.NET Core section before the token-based Antiforgery sections, gated to >=11.0 inside the existing >=8.0 moniker zone (zones are split, not nested). Delete csrf-protection.md, add redirects to anti-request-forgery, remove the toc entry, and repoint all inbound xrefs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .openpublishing.redirection.json | 7 +- aspnetcore/blazor/forms/index.md | 2 +- aspnetcore/blazor/security/index.md | 2 +- ...d-cross-site-request-forgery-protection.md | 2 +- .../migration/100-to-110/includes/security.md | 2 +- .../aspnetcore-11/includes/blazor.md | 4 +- aspnetcore/security/anti-request-forgery.md | 340 ++++++++++++++++- aspnetcore/security/csrf-protection.md | 345 ------------------ aspnetcore/toc.yml | 2 - 9 files changed, 351 insertions(+), 355 deletions(-) delete mode 100644 aspnetcore/security/csrf-protection.md diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 4739e5ed748e..5af8af11e3d6 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -572,7 +572,12 @@ }, { "source_path": "aspnetcore/migration/antiforgery-to-csrf.md", - "redirect_url": "/aspnet/core/security/csrf-protection", + "redirect_url": "/aspnet/core/security/anti-request-forgery", + "redirect_document_id": false + }, + { + "source_path": "aspnetcore/security/csrf-protection.md", + "redirect_url": "/aspnet/core/security/anti-request-forgery", "redirect_document_id": false }, { diff --git a/aspnetcore/blazor/forms/index.md b/aspnetcore/blazor/forms/index.md index 848401696551..09708ee711dc 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 in ASP.NET Core](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..9880e9455f9a 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 in ASP.NET Core](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 6efa5a7e4502..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/includes/security.md b/aspnetcore/migration/100-to-110/includes/security.md index 9af90c7f0ee9..ad2b5dd4cf1e 100644 --- a/aspnetcore/migration/100-to-110/includes/security.md +++ b/aspnetcore/migration/100-to-110/includes/security.md @@ -19,4 +19,4 @@ A `400 Bad Request` on a cross-origin form post is the CSRF protection working a * 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 like a bearer token. -For a full description of the middleware, the validation rules, and how it interacts with the token-based antiforgery system, see . +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/release-notes/aspnetcore-11/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md index a8a549055c32..7e88ed00c52c 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md @@ -790,7 +790,7 @@ 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: @@ -799,7 +799,7 @@ For more information, see the following resources: 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..2ff1839a4863 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,344 @@ 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 + +Starting in .NET 11, 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), 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). + +### 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 `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 [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 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. Both systems protect the same form-handling endpoints, so the automatic middleware is often enough on its own. + +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 `IAntiforgeryAdditionalDataProvider` to round-trip extra data inside the token. +* A security review or compliance requirement specifies the token defense as an independent layer. + +Consider dropping the token-based system when the app targets modern evergreen browsers only and defense in depth at the token layer isn't a hard requirement. To drop it, remove the `app.UseAntiforgery()` and `AddAntiforgery(...)` calls along with the token plumbing: `@Html.AntiForgeryToken()` in views, the `RequestVerificationToken` header in client code, and the `[ValidateAntiForgeryToken]` and `[AutoValidateAntiforgeryToken]` attributes. Per-endpoint opt-outs (`.DisableAntiforgery()`, `[IgnoreAntiforgeryToken]`) can stay — they also opt the endpoint out of the automatic middleware. + +For the .NET 10 to .NET 11 upgrade walkthrough, 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. + +The token-based antiforgery system described in the rest of this article predates this middleware and remains available. Use it in the scenarios listed in [Adopting CSRF-only protection in existing apps](#adopting-csrf-only-protection-in-existing-apps); for most apps, the automatic protection is enough on its own. + +:::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 d06a956c12f1..000000000000 --- a/aspnetcore/security/csrf-protection.md +++ /dev/null @@ -1,345 +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: 07/15/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. Both systems protect the same form-handling endpoints, so the automatic middleware is often enough on its own. - -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 `IAntiforgeryAdditionalDataProvider` to round-trip extra data inside the token. -* A security review or compliance requirement specifies the token defense as an independent layer. - -Consider dropping the token-based system when the app targets modern evergreen browsers only and defense in depth at the token layer isn't a hard requirement. To drop it, remove the `app.UseAntiforgery()` and `AddAntiforgery(...)` calls along with the token plumbing: `@Html.AntiForgeryToken()` in views, the `RequestVerificationToken` header in client code, and the `[ValidateAntiForgeryToken]` and `[AutoValidateAntiforgeryToken]` attributes. Per-endpoint opt-outs (`.DisableAntiforgery()`, `[IgnoreAntiforgeryToken]`) can stay — they also opt the endpoint out of the automatic middleware. - -For the .NET 10 to .NET 11 upgrade walkthrough, 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. -* — the .NET 10 to .NET 11 migration guide, including CSRF adoption steps. -* — 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 4027b20e0f97..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) From ac88bc26941ea8faf39bf30000f26541eeabd916 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 15 Jul 2026 18:10:49 +0200 Subject: [PATCH 06/11] Refer to all Blazor SSR endpoints rather than only SSR form posts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aspnetcore/security/anti-request-forgery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/security/anti-request-forgery.md b/aspnetcore/security/anti-request-forgery.md index 2ff1839a4863..b2a60530e0d6 100644 --- a/aspnetcore/security/anti-request-forgery.md +++ b/aspnetcore/security/anti-request-forgery.md @@ -122,7 +122,7 @@ The following components read `IAntiforgeryValidationFeature` and reject a reque * MVC actions protected by antiforgery. * Minimal API endpoints that bind a form parameter. -* Blazor static server-side rendering (SSR) form posts. +* 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. From de095486fcb30318f1298815f1b82eb5d8333668 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Thu, 16 Jul 2026 11:13:41 +0200 Subject: [PATCH 07/11] review 1 --- .openpublishing.redirection.json | 10 ---------- aspnetcore/blazor/forms/index.md | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 5af8af11e3d6..5dd4f1a65a8d 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -570,16 +570,6 @@ "redirect_url": "/aspnet/core", "redirect_document_id": false }, - { - "source_path": "aspnetcore/migration/antiforgery-to-csrf.md", - "redirect_url": "/aspnet/core/security/anti-request-forgery", - "redirect_document_id": false - }, - { - "source_path": "aspnetcore/security/csrf-protection.md", - "redirect_url": "/aspnet/core/security/anti-request-forgery", - "redirect_document_id": false - }, { "source_path": "aspnetcore/migration/rc1-to-rtm.md", "redirect_url": "/aspnet/core/migration/", diff --git a/aspnetcore/blazor/forms/index.md b/aspnetcore/blazor/forms/index.md index 09708ee711dc..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 [Automatic CSRF protection in ASP.NET Core](xref:security/anti-request-forgery#automatic-csrf-protection-in-aspnet-core). +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. From 55fe19692e18cbbbbbac8c9b16679530cd2c18da Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Thu, 16 Jul 2026 11:15:33 +0200 Subject: [PATCH 08/11] review 2 --- aspnetcore/blazor/security/index.md | 2 +- aspnetcore/migration/100-to-110/includes/security.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aspnetcore/blazor/security/index.md b/aspnetcore/blazor/security/index.md index 9880e9455f9a..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 [Automatic CSRF protection in ASP.NET Core](xref:security/anti-request-forgery#automatic-csrf-protection-in-aspnet-core). +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/migration/100-to-110/includes/security.md b/aspnetcore/migration/100-to-110/includes/security.md index ad2b5dd4cf1e..74cbf88cee45 100644 --- a/aspnetcore/migration/100-to-110/includes/security.md +++ b/aspnetcore/migration/100-to-110/includes/security.md @@ -2,7 +2,7 @@ .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: +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. @@ -10,13 +10,13 @@ The middleware validates endpoints that opt in to antiforgery validation — tha 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 `IAntiforgeryAdditionalDataProvider`, or must keep the token defense as an independent layer for a compliance requirement. Both protections can coexist. +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 `AddAntiforgery()` and `UseAntiforgery()` 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). +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 like a bearer token. +* 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). From 5a1dca24ea6aaab6f0a0f2984ee8667b1a6282ca Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Thu, 16 Jul 2026 11:15:52 +0200 Subject: [PATCH 09/11] review 3 --- aspnetcore/release-notes/aspnetcore-11/includes/blazor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md index 7e88ed00c52c..d3a5884b0215 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md @@ -790,7 +790,7 @@ For more information, see [Fix TempData and SupplyParameterFromSession persisten ### Redundant `app.UseAntiforgery()` removed from Blazor Web App templates -[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. +[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: From 5114f5cea57226eea60c2854e635f31c840f8bd7 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Thu, 16 Jul 2026 11:21:45 +0200 Subject: [PATCH 10/11] review 4 --- aspnetcore/security/anti-request-forgery.md | 78 +++++++++------------ 1 file changed, 32 insertions(+), 46 deletions(-) diff --git a/aspnetcore/security/anti-request-forgery.md b/aspnetcore/security/anti-request-forgery.md index b2a60530e0d6..809bb7ab7063 100644 --- a/aspnetcore/security/anti-request-forgery.md +++ b/aspnetcore/security/anti-request-forgery.md @@ -89,13 +89,13 @@ Attacks that exploit trusted cookies between apps hosted on the same domain can ### 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. +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. +`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 -Starting in .NET 11, 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), 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. +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. @@ -103,10 +103,10 @@ This middleware is *additive* to the token-based antiforgery system. The two pro ### 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: +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. **`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). @@ -116,9 +116,9 @@ The middleware records this verdict on the request rather than ending the reques ### 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 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 `IAntiforgeryValidationFeature` and reject a request with `400` when the recorded verdict is invalid: +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. @@ -127,7 +127,7 @@ The following components read `IAntiforgeryValidationFeature` and reject a reque 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. +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 @@ -145,18 +145,18 @@ app.MapPost("/widgets", ([FromForm] Widget 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. +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. +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. +* **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`. +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. @@ -197,15 +197,15 @@ app.MapPost("/widgets", ([FromForm] Widget w) => Results.Created($"/widgets/{w.I ``` > [!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. +> `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 . +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. +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: @@ -230,11 +230,11 @@ public class WebhookController : ControllerBase 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 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. +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`: @@ -253,7 +253,7 @@ 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. +> 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 @@ -261,7 +261,7 @@ When this key is set to `true`, `WebApplication` skips registering the middlewar 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. +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` @@ -282,7 +282,7 @@ To replace the default implementation, register a singleton in DI. Because the f 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: +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; @@ -316,11 +316,11 @@ public sealed class AllowlistCsrfProtection : ICsrfProtection } ``` -The middleware still honors `.DisableAntiforgery()` / `[IgnoreAntiforgeryToken]` regardless of which implementation is registered — opt-out is handled by the middleware itself before `ValidateAsync` is called. +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. +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 | |---|---|---| @@ -337,7 +337,7 @@ The token-based middleware specifically protects against the classic CSRF attack * **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 [Antiforgery in ASP.NET Core](#antiforgery-in-aspnet-core). +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 @@ -348,36 +348,22 @@ When an app calls `app.UseAntiforgery()`, the token-based middleware runs after 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 -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. +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. +* 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. Both systems protect the same form-handling endpoints, so the automatic middleware is often enough on its own. - -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 `IAntiforgeryAdditionalDataProvider` to round-trip extra data inside the token. -* A security review or compliance requirement specifies the token defense as an independent layer. - -Consider dropping the token-based system when the app targets modern evergreen browsers only and defense in depth at the token layer isn't a hard requirement. To drop it, remove the `app.UseAntiforgery()` and `AddAntiforgery(...)` calls along with the token plumbing: `@Html.AntiForgeryToken()` in views, the `RequestVerificationToken` header in client code, and the `[ValidateAntiForgeryToken]` and `[AutoValidateAntiforgeryToken]` attributes. Per-endpoint opt-outs (`.DisableAntiforgery()`, `[IgnoreAntiforgeryToken]`) can stay — they also opt the endpoint out of the automatic middleware. - -For the .NET 10 to .NET 11 upgrade walkthrough, see . +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` with no body. +**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`. This is the expected default behavior for endpoints that process forms. +**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: @@ -413,9 +399,9 @@ curl -i -X POST https://localhost:{PORT}/widgets \ -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. +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. Use it in the scenarios listed in [Adopting CSRF-only protection in existing apps](#adopting-csrf-only-protection-in-existing-apps); for most apps, the automatic protection is enough on its own. +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 From ffc34a602f27a04eb24ddf29de39602c524f57d0 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Thu, 16 Jul 2026 11:29:43 +0200 Subject: [PATCH 11/11] move it --- aspnetcore/security/anti-request-forgery.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aspnetcore/security/anti-request-forgery.md b/aspnetcore/security/anti-request-forgery.md index 809bb7ab7063..a6d226e4a0d2 100644 --- a/aspnetcore/security/anti-request-forgery.md +++ b/aspnetcore/security/anti-request-forgery.md @@ -337,6 +337,12 @@ The token-based middleware specifically protects against the classic CSRF attack * **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