[breaking] Defer antiforgery/CSRF rejection to form consumers via IAntiforgeryValidationFeature#67082
Conversation
|
Thanks for identifying a breaking change. @DeagleGross, after you commit this PR please take the following actions, as part of the breaking changes announcement process: |
There was a problem hiding this comment.
Pull request overview
This PR changes Razor Components antiforgery behavior to defer to the upstream antiforgery/CSRF middleware “verdict” (via IAntiforgeryValidationFeature) instead of directly validating tokens inside the RazorComponentEndpointInvoker. It also introduces shared HttpContext.Items marker keys so downstream components can detect whether either Antiforgery or CSRF middleware ran for the matched endpoint.
Changes:
- Introduce shared
MiddlewareInvokedKeysand have Antiforgery/CSRF middleware set endpoint-invoked markers inHttpContext.Items. - Update
EndpointMiddleware’s “missing antiforgery middleware” safety check to accept either the Antiforgery marker or the CSRF marker. - Update Razor Components endpoint invocation and token emission to rely on middleware-set validation state and to mint/store tokens only when Antiforgery middleware actually ran.
Show a summary per file
| File | Description |
|---|---|
| src/Shared/MiddlewareInvokedKeys.cs | Adds shared HttpContext.Items keys (and sentinel) for middleware-invoked markers. |
| src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs | Adds unit coverage for the CSRF marker satisfying the antiforgery-middleware presence check; updates existing key usage. |
| src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj | Links the new shared MiddlewareInvokedKeys.cs into the Routing assembly. |
| src/Http/Routing/src/EndpointMiddleware.cs | Accepts either Antiforgery or CSRF marker as satisfying the antiforgery middleware requirement check. |
| src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/CsrfProtectionIntegrationTests.cs | Adds integration tests asserting CSRF middleware sets the marker when an endpoint is matched (including when antiforgery is disabled on the endpoint). |
| src/DefaultBuilder/src/Microsoft.AspNetCore.csproj | Links the new shared MiddlewareInvokedKeys.cs into the DefaultBuilder assembly. |
| src/DefaultBuilder/src/Internal/CsrfProtectionMiddleware.cs | Sets the CSRF marker when an endpoint is present before performing validation/skip logic. |
| src/Components/Endpoints/test/RazorComponentEndpointInvokerTest.cs | Adds tests for invoker behavior when IAntiforgeryValidationFeature is valid/invalid/absent. |
| src/Components/Endpoints/test/Forms/EndpointAntiforgeryStateProviderTest.cs | Adds tests to ensure token generation is gated on the Antiforgery marker and can be disabled. |
| src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs | Removes endpoint-level antiforgery validation; relies on IAntiforgeryValidationFeature and gates token storage on the Antiforgery marker. |
| src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj | Links the new shared MiddlewareInvokedKeys.cs into the Components Endpoints assembly. |
| src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs | Gates _canGenerateToken on the Antiforgery marker instead of assuming token generation is always possible. |
| src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery.csproj | Links the new shared MiddlewareInvokedKeys.cs into the Antiforgery assembly. |
| src/Antiforgery/src/AntiforgeryMiddleware.cs | Sets the shared Antiforgery marker in HttpContext.Items when an endpoint is present. |
Copilot's findings
- Files reviewed: 14/14 changed files
- Comments generated: 2
|
@DeagleGross might be worth looking also at AntiforgeryToken (the component in Blazor). Not sure what happens if AF is disabled and the component is rendered into the page. |
aspnetcore/src/Components/Web/src/Forms/AntiforgeryToken.cs Lines 31 to 34 in da04fcb I've verified that having |
IAntiforgeryValidationFeature
Co-authored-by: Copilot <copilot@github.com>
|
/ba-g unrelated: macOS quarantined-pr job timeouts |
After merging dotnet#67082 the CSRF middleware records a verdict on IAntiforgeryValidationFeature instead of short-circuiting. Update the in-code rationale to describe the wrong-verdict symptom and that matching the endpoint also sets the CsrfProtection sentinel EndpointMiddleware requires. Comment-only; behavior unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ock (dotnet#67174) Two review refinements: 1. CsrfProtectionMiddleware: revert to the dotnet#67082 baseline exactly. The deferred post-routing block guarantees routing runs before CSRF, so the middleware needs no dotnet#67174-specific changes. Removes the unnecessary RecordVerdictAsync extraction introduced during the earlier reroute exploration; InvokeAsync once again records the verdict and calls next inline. Zero diff versus dotnet#67082. 2. WebApplicationBuilder: the post-routing block carries authentication, authorization, and CSRF together (not CSRF alone). When the app calls UseRouting() explicitly, all three run immediately after the endpoint is matched - identical to the order used when the framework injects UseRouting() itself. This makes explicit and implicit UseRouting() behave the same and is what fixes the per-endpoint named CORS policy case (dotnet#67174). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#67174) Auth/authz are security-critical and already work correctly in every case, so they keep their established position at the head of the destination pipeline - unchanged from main. Only CSRF protection is deferred into the post-routing block, because CSRF is the single middleware that must observe the matched endpoint (to read a per-endpoint CORS policy such as RequireCors("name"), dotnet#67174). Rationale: a user middleware placed before an explicit UseRouting() still sees auth-populated context.User exactly as today, and the existing loud guard in EndpointMiddleware (throws when an [Authorize] endpoint is hit without authorization having run on it) is preserved. This keeps the fix surgical and avoids any change to security-sensitive auth ordering. The post-routing block helper stays general (authn/authz/csrf parameters) so future middleware can opt into running after routing. CsrfProtectionMiddleware is unchanged from dotnet#67082. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…yMetadata` (#67460) Aligns CsrfProtectionMiddleware with classic AntiforgeryMiddleware: validation runs only when the matched endpoint has IAntiforgeryMetadata { RequiresValidation: true }. Endpoints without metadata (plain MapPost, plain [HttpPost]) pass through, matching .NET 10 behavior — so no customer endpoint that worked on .NET 10 will start failing on .NET 11. Blazor SSR and RDF form binding stay protected because they already attach RequireAntiforgeryTokenAttribute automatically. The MiddlewareInvokedKeys.CsrfProtection sentinel is still set unconditionally to preserve the FormFeature backstop contract from #67082.
…yMetadata` (#67460) Aligns CsrfProtectionMiddleware with classic AntiforgeryMiddleware: validation runs only when the matched endpoint has IAntiforgeryMetadata { RequiresValidation: true }. Endpoints without metadata (plain MapPost, plain [HttpPost]) pass through, matching .NET 10 behavior — so no customer endpoint that worked on .NET 10 will start failing on .NET 11. Blazor SSR and RDF form binding stay protected because they already attach RequireAntiforgeryTokenAttribute automatically. The MiddlewareInvokedKeys.CsrfProtection sentinel is still set unconditionally to preserve the FormFeature backstop contract from #67082.
|
The current implementation recognizes only the traditional safe HTTP methods (e.g., GET and HEAD). However, recent versions of ASP.NET introduce the QUERY HTTP method. Since QUERY is not included in the current list of safe methods, it is incorrectly treated as a potentially unsafe method. The implementation should be updated to recognize QUERY as a safe HTTP method. |
|
Please file an issue, we don't track comments on closed PRs. |
Summary
CsrfProtectionMiddlewareno longer short-circuits a cross-origin request. Instead it records its verdict onIAntiforgeryValidationFeature— the same feature the token-basedAntiforgeryMiddlewareuses — and lets the request continue. Downstream components read that verdict and reject the request when they actually consume the form. This unifies cross-origin CSRF and token antiforgery behind a single verdict, and makes consumers (like Razor Components) defer to whichever middleware ran instead of validating on their own.Validation is lazy (forms vs. not touching forms)
A recorded
IsValid == falseverdict does nothing on its own — the request is only rejected when a component reads the form (or otherwise inspects the feature):[FromForm], Blazor SSR form POST): the invalid verdict is observed and the request is rejected with400.Where the verdict is read and the request is rejected
Four consumers read
IAntiforgeryValidationFeatureand reject onIsValid == false. Each first confirms a middleware actually ran (anHttpContext.Itemsmarker for Antiforgery or CSRF) before trusting the verdict:AntiforgeryMiddlewareAuthorizationFilter— MVCRequestDelegateFactory— minimal APIs binding[FromForm]FormFeature— backstop when the form is read directlyRazorComponentEndpointInvoker— Blazor SSRToken validation overrides CSRF
When an app calls
UseAntiforgery(), the token middleware runs after CSRF and is authoritative: it clears any prior CSRF verdict and re-records the result of token validation. A request CSRF-marked invalid can be overridden to valid by a successful token check, and vice versa.Razor Components behavior change (breaking)
RazorComponentEndpointInvokerno longer callsIAntiforgery.ValidateRequestAsyncitself. It trusts the verdict left by the upstream middleware and returns400only whenIAntiforgeryValidationFeature.IsValid == false. It also skips antiforgery token generation when no token middleware ran. Apps which callsUseAntiforgery()see no change. Apps that removedUseAntiforgery()(see #67119) are now protected by the auto-injected CSRF middleware instead of token antiforgery.flowchart TB R[Request] --> CSRF[CsrfProtectionMiddleware<br/>auto-injected · records verdict on IAntiforgeryValidationFeature · no short-circuit] CSRF --> AF[UseAntiforgery optional<br/>token validation overrides the CSRF verdict] AF --> ROUTING[Routing · matches endpoint] ROUTING --> EM[EndpointMiddleware<br/>safety check: AF or CSRF marker present?] EM --> INV[RazorComponentEndpointInvoker<br/>reads IAntiforgeryValidationFeature → 400 if IsValid is false] INV --> RND[EndpointHtmlRenderer · renders component tree] RND --> SP[EndpointAntiforgeryStateProvider<br/>AF marker → can generate token?] SP --> TOK[<AntiforgeryToken /> · emits __RequestVerificationToken input]Notes
UseAntiforgery()still throws on a required endpoint when neither middleware ran (e.g.DisableCsrfProtection=true, or a non-WebApplicationhost). TheEndpointMiddlewaremarker check is what enforces this.Related #51981
Closes #67081