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 new file mode 100644 index 000000000000..311ea7d00a10 --- /dev/null +++ b/aspnetcore/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection.md @@ -0,0 +1,61 @@ +--- +title: "Breaking change: Blazor server-side rendering defers antiforgery validation to middleware" +ai-usage: ai-assisted +description: "Learn about the breaking change in ASP.NET Core 11 where Blazor static server-side rendering relies on antiforgery middleware to validate requests and generate tokens instead of validating them in the endpoint." +ms.date: 06/17/2026 +--- +# Blazor server-side rendering defers antiforgery validation to middleware + +In ASP.NET Core 11, Blazor static server-side rendering (SSR) endpoints no longer validate antiforgery tokens themselves. They rely on the antiforgery middleware to record a validation result, and they generate antiforgery tokens only when the token-based antiforgery middleware is present in the pipeline. + +## Version introduced + +.NET 11 + +## Previous behavior + +Previously, when a Blazor SSR endpoint handled a form `POST`, the Razor Components endpoint validated the request itself. If the upstream antiforgery middleware hadn't already recorded a result, the endpoint called `IAntiforgery` directly to validate the request's antiforgery token. The endpoint also always generated and stored antiforgery tokens for rendered forms, whether or not `app.UseAntiforgery()` was in the pipeline. + +As a result, a Blazor SSR app that didn't call `app.UseAntiforgery()` still had its form posts validated against antiforgery tokens by the endpoint, and still emitted tokens for its forms. + +## New behavior + +Starting in ASP.NET Core 11, the Razor Components endpoint trusts the antiforgery verdict recorded on the request's `IAntiforgeryValidationFeature` by upstream middleware. For a form `POST`, it returns `400 Bad Request` only when the recorded verdict is invalid, and it no longer calls `IAntiforgery` to validate the request itself. The endpoint generates antiforgery tokens only when the token-based antiforgery middleware ran for the request; when that middleware isn't present, the endpoint skips token generation. + +The verdict can be recorded by either of two middleware components: + +* The token-based antiforgery middleware that `app.UseAntiforgery()` adds. +* The automatic cross-origin CSRF protection middleware that's injected by default in apps built with `WebApplication.CreateBuilder` (new in .NET 11). + +The impact depends on the app's configuration: + +* Apps that call `app.UseAntiforgery()` are unaffected. Requests are validated against antiforgery tokens, and tokens are generated for forms, exactly as before. +* Apps that don't call `app.UseAntiforgery()` are now protected by the automatic CSRF protection middleware instead of by token validation in the endpoint. These apps no longer emit antiforgery tokens for their forms. + +## Type of breaking change + +This change is a [behavioral change](/dotnet/core/compatibility/categories#behavioral-change). + +## Reason for change + +The token-based antiforgery system and the new cross-origin CSRF protection now record a single validation result on the shared `IAntiforgeryValidationFeature`, which form-consuming components read to decide whether to reject a request. Having the Razor Components endpoint validate the request a second time duplicated that work and could produce a result that differed from the middleware's. Generating tokens when no antiforgery middleware was present produced tokens that nothing validated. + +For more information, see [dotnet/aspnetcore#67082](https://github.com/dotnet/aspnetcore/pull/67082). + +## Recommended action + +If your Blazor SSR app relies on antiforgery tokens — for example, to validate form posts or to render tokens into forms — make sure it calls `app.UseAntiforgery()`: + +```csharp +var app = builder.Build(); + +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 . + +## Affected APIs + +None. No public API surface changed. The change affects the behavior of Blazor static server-side rendering endpoints and the antiforgery state provider that generates tokens. diff --git a/aspnetcore/breaking-changes/11/overview.md b/aspnetcore/breaking-changes/11/overview.md index da30fc09b8cc..e1f466fc1e2d 100644 --- a/aspnetcore/breaking-changes/11/overview.md +++ b/aspnetcore/breaking-changes/11/overview.md @@ -1,10 +1,10 @@ --- title: Breaking changes in ASP.NET Core 11 ai-usage: ai-assisted -titleSuffix: "" description: Navigate to the breaking changes in ASP.NET Core 11. -ms.date: 06/04/2026 +ms.date: 06/25/2026 no-loc: [Blazor, Kestrel, SignalR] +titleSuffix: "" --- # Breaking changes in ASP.NET Core 11 @@ -16,6 +16,7 @@ If you're migrating an app to ASP.NET Core 11, the breaking changes listed here |-------|-------------------| | [Blazor custom event registration throws when name matches a browser event](blazor-custom-event-name-collision.md) | Behavioral change | | [Blazor enhanced navigation no longer preloads resources](blazor-enhanced-nav-preloading-disabled.md) | Behavioral change | +| [Blazor server-side rendering defers antiforgery validation to middleware](blazor-server-side-rendering-deferred-cross-site-request-forgery-protection.md) | Behavioral change | | [ConcurrencyLimiter middleware removed](concurrencylimiter-removed.md) | Binary/source incompatible | | [Hosting emits OpenTelemetry HTTP semantic-convention tags by default](http-activity-otel-semconv.md) | Behavioral change | | [Kestrel tightens HTTP protocol compliance](kestrel-strict-protocol-compliance.md) | Behavioral change | diff --git a/aspnetcore/migration/antiforgery-to-csrf.md b/aspnetcore/migration/antiforgery-to-csrf.md index 8443cebbc8ec..7e5aa5edeace 100644 --- a/aspnetcore/migration/antiforgery-to-csrf.md +++ b/aspnetcore/migration/antiforgery-to-csrf.md @@ -5,7 +5,7 @@ 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 +ms.date: 06/25/2026 uid: migration/antiforgery-to-csrf --- # Adopt automatic CSRF protection in .NET 11 @@ -50,6 +50,8 @@ For apps that fit the second list above, the automatic CSRF middleware alone is Per-endpoint opt-outs (`.DisableAntiforgery()` on Minimal APIs, `[IgnoreAntiforgeryToken]` on MVC) can stay where they are — both also opt the endpoint out of the new CSRF middleware. Remove them only if the endpoint should be protected after the simplification. +For Blazor static server-side rendering (SSR), removing `app.UseAntiforgery()` is a breaking change that also stops antiforgery token generation. See [Blazor static server-side rendering](#blazor-static-server-side-rendering) before removing it. + ### Before / after Minimal API: @@ -105,22 +107,31 @@ public class WidgetsController : ControllerBase Cross-origin SPAs still need a CORS policy listing the trusted origin, regardless of which antiforgery model the app uses. See [Allowing cross-origin clients](xref:security/csrf-protection#allowing-cross-origin-clients) for the resolution rules. +### Blazor static server-side rendering + +Removing `app.UseAntiforgery()` is a breaking change for Blazor static server-side rendering (SSR). Without the token-based middleware: + +* Form posts are validated by the automatic CSRF middleware using `Sec-Fetch-Site` and `Origin` instead of antiforgery tokens. Blazor SSR endpoints now trust the verdict recorded by the upstream middleware rather than validating the request themselves. +* Blazor stops generating antiforgery tokens for rendered forms, because no middleware is present to validate a token on a later request. + +This is appropriate for apps that target modern browsers and rely on the header-based defense. Apps that need antiforgery tokens — for example, to support browsers that don't send `Sec-Fetch-Site` — should keep `app.UseAntiforgery()`. For the formal breaking-change notice, see [Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection). + ## After upgrade: if requests start failing -The automatic CSRF middleware is enabled by default in .NET 11. Same-origin browser requests and non-browser clients (`curl`, server-to-server, mobile apps) are unaffected. Cross-origin browser write requests that aren't covered by a CORS policy are rejected with HTTP `400`. +The automatic CSRF middleware is enabled by default in .NET 11. Same-origin browser requests and non-browser clients (`curl`, server-to-server, mobile apps) are unaffected. Cross-origin browser form posts that aren't covered by a CORS policy are rejected with HTTP `400` when the endpoint processes the form. ### Symptoms -Cross-origin browser 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. +Cross-origin browser form posts that succeeded on .NET 10 fail on .NET 11 with HTTP `400 Bad Request` and an empty response body when the endpoint binds or reads the form. The form processing is rejected before the handler body runs. Endpoints that don't read form data — such as JSON APIs — aren't affected, because the recorded verdict is only enforced by form consumers. The same request issued from `curl` or another non-browser client typically succeeds, which is a useful quick check that distinguishes the new middleware from other causes: ```bash -# In a browser at https://app.contoso.com posting to https://api.contoso.com: 400 +# In a browser at https://app.contoso.com posting a form to https://api.contoso.com: 400 # From a terminal, the same request: 200/201 (no Sec-Fetch-Site, no Origin → treated as non-browser) curl -i -X POST https://api.contoso.com/widgets \ - -H "Content-Type: application/json" \ - -d '{"name":"test"}' + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "name=test" ``` ### Confirm with logs @@ -137,11 +148,11 @@ To confirm the rejection comes from the CSRF middleware, enable `Debug` logging } ``` -A denied request appears as: +A recorded verdict appears as: ```text dbug: Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware[1] - Cross-origin CSRF protection denied request POST /widgets from origin 'https://app.contoso.com'. + Cross-origin CSRF protection marked request POST /widgets from origin 'https://app.contoso.com' as invalid. ``` ### Fix A: allow the origin via CORS @@ -206,9 +217,13 @@ If the upgrade window is tight and individual fixes will take time, the middlewa This is an escape hatch, not a recommended end state. Re-enable as soon as CORS and per-endpoint opt-outs are in place. +> [!WARNING] +> The automatic CSRF middleware also satisfies the antiforgery requirement for endpoints that require validation, even when an app doesn't call `app.UseAntiforgery()`. If an app relies on antiforgery but doesn't call `app.UseAntiforgery()`, disabling the CSRF middleware with `DisableCsrfProtection` removes the only antiforgery middleware in the pipeline. A request to an endpoint that requires validation then throws an exception. The same is true when the app runs on a host that isn't built with `WebApplication`, where the CSRF middleware isn't injected. In either configuration, add `app.UseAntiforgery()` so a middleware is present to validate the request. + ## Related * — full reference for the new middleware. * — the token-based antiforgery system. * — CORS configuration reference. +* [Breaking change: Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection) * — overall .NET 10 → .NET 11 migration guide. diff --git a/aspnetcore/security/csrf-protection.md b/aspnetcore/security/csrf-protection.md index 67585ab962bb..e55fc86ee489 100644 --- a/aspnetcore/security/csrf-protection.md +++ b/aspnetcore/security/csrf-protection.md @@ -2,54 +2,72 @@ 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 reject cross-origin form posts by default. monikerRange: '>= aspnetcore-11.0' ms.author: tdykstra ms.custom: mvc -ms.date: 06/05/2026 +ms.date: 06/25/2026 uid: security/csrf-protection --- # Automatic CSRF protection in ASP.NET Core -Starting in .NET 11, ASP.NET Core ships an automatic Cross-Site Request Forgery (CSRF) protection middleware that's enabled by default in apps built with `WebApplication.CreateBuilder`. Unlike the [token-based antiforgery system](xref:security/anti-request-forgery), this middleware doesn't issue or validate tokens. Instead, it inspects the [Fetch Metadata](https://developer.mozilla.org/docs/Web/HTTP/Headers/Sec-Fetch-Site) headers that modern browsers attach to every request 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, 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 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. +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 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: +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 rejected. +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. -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 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 like the following is already protected: +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("/api/widgets", (Widget w) => Results.Created($"/api/widgets/{w.Id}", w)); +app.MapPost("/widgets", ([FromForm] Widget w) => + Results.Created($"/widgets/{w.Id}", w)); 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. +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: @@ -58,9 +76,9 @@ The middleware integrates with the existing antiforgery model: ## 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 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 allows the request. +The CSRF middleware doesn't introduce its own trust list. It reuses the same CORS policy that the [CORS middleware](xref:security/cors) resolves for the endpoint: if that policy allows the request's `Origin`, the CSRF middleware records an allowed verdict for the request. The policy is picked per-endpoint in this order: @@ -85,7 +103,8 @@ var app = builder.Build(); app.UseCors(); -app.MapPost("/api/widgets", (Widget w) => Results.Created($"/api/widgets/{w.Id}", w)); +app.MapPost("/widgets", ([FromForm] Widget w) => + Results.Created($"/widgets/{w.Id}", w)); app.Run(); ``` @@ -93,7 +112,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("/widgets", ([FromForm] Widget w) => Results.Created($"/widgets/{w.Id}", w)) .RequireCors("api"); ``` @@ -153,6 +172,9 @@ 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). @@ -218,7 +240,7 @@ The middleware still honors `.DisableAntiforgery()` / `[IgnoreAntiforgeryToken]` ## Interaction with token-based antiforgery -The two CSRF defenses target different layers and are designed to coexist: +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 | |---|---|---| @@ -231,21 +253,41 @@ 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. +* **Razor Pages, MVC, and Blazor SSR apps** that already use the token system gain a header-based check that runs before token validation, without changing the token flow. +* **Minimal API apps** that bind forms get a useful default without needing to call `app.UseAntiforgery()` or thread `IAntiforgery` through endpoints. * **APIs called from cross-origin SPAs** can rely on this middleware combined with a CORS allowlist, and skip the token system entirely if the API never serves HTML forms. For details on the token-based system — including form integration, AJAX flows, configuration via `AntiforgeryOptions`, and `IAntiforgery` APIs — see . +### Token validation takes precedence + +When an app calls `app.UseAntiforgery()`, the token-based middleware runs after the automatic CSRF middleware. The token middleware clears any verdict the CSRF middleware recorded and replaces it with the result of token validation. The token result is authoritative: + +* A request that the CSRF middleware marked invalid becomes valid if it carries a valid token. +* A request that the CSRF middleware allowed is marked invalid if its token is missing or invalid. + +This ordering means apps that use the token system see the same end-to-end behavior they had before the automatic middleware existed, while apps that don't use tokens fall back to the CSRF middleware's verdict. + +## Blazor server-side rendering + +Blazor static server-side rendering (SSR) endpoints participate in the same deferred model. The Razor Components endpoint trusts the verdict recorded on `IAntiforgeryValidationFeature` by the upstream middleware and returns `400 Bad Request` for a form post only when that verdict is invalid. The endpoint no longer validates the request itself. + +The behavior depends on which middleware ran: + +* Apps that call `app.UseAntiforgery()` are unchanged. The token-based middleware validates each request, and antiforgery tokens are generated for rendered forms as before. +* Apps that don't call `app.UseAntiforgery()` are protected by the automatic CSRF middleware instead. In that configuration, the endpoint skips antiforgery token generation, because no token middleware is present to validate a token on a later request. + +This is a behavior change for Blazor SSR apps that previously removed `app.UseAntiforgery()`: they're now protected by the CSRF middleware rather than left unprotected, and they stop emitting antiforgery tokens. For migration guidance, see . For the formal breaking-change notice, see [Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection). + ## Adopting CSRF-only protection in existing apps For apps that are upgrading from .NET 10 and already use the token-based system, the automatic CSRF middleware can replace it entirely in many scenarios. For guidance on when to drop the token-based system and how to do so, see . ## Troubleshooting -**Symptom:** Same-origin requests from a browser succeed, but cross-origin requests return `400` with no body. +**Symptom:** Same-origin requests from a browser succeed, but cross-origin form posts 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 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: @@ -253,7 +295,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 { @@ -265,28 +307,29 @@ For apps that are upgrading from .NET 10 and already use the token-based system, } ``` -A denial then appears in the log as: +A recorded verdict 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 /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: +**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}/api/widgets \ +curl -i -X POST https://localhost:{PORT}/widgets \ -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. +Replace `{PORT}` with the app's local HTTPS port. The `400` is observed because the endpoint binds the form, which enforces the recorded verdict. A non-form endpoint returns its normal response, because nothing reads the verdict. Without the `Origin` header, the same request is allowed because `curl` doesn't send `Sec-Fetch-Site` either, and a request with neither header is treated as a non-browser client. ## Additional resources * — the token-based antiforgery system, including form integration and `IAntiforgery` APIs. * — when and how to drop the token-based system in favor of the automatic middleware. * — configuring CORS policies, which this middleware uses as its trusted-origin source. +* [Breaking change: Blazor server-side rendering defers antiforgery validation to middleware](/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection) * [MDN — `Sec-Fetch-Site`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Sec-Fetch-Site) * [MDN — `Origin`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Origin) diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 98ceb9cf43bf..ebcd493ef46b 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -19,6 +19,8 @@ items: href: breaking-changes/11/blazor-custom-event-name-collision.md - name: Blazor enhanced navigation no longer preloads resources href: breaking-changes/11/blazor-enhanced-nav-preloading-disabled.md + - name: Blazor server-side rendering defers antiforgery validation to middleware + href: breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection.md - name: ConcurrencyLimiter middleware removed href: breaking-changes/11/concurrencylimiter-removed.md - name: Hosting emits OpenTelemetry HTTP semantic-convention tags by default