Skip to content

[breaking] Defer antiforgery/CSRF rejection to form consumers via IAntiforgeryValidationFeature#67082

Merged
DeagleGross merged 7 commits into
dotnet:mainfrom
DeagleGross:dmkorolev/csrf-in-blazor
Jun 18, 2026
Merged

[breaking] Defer antiforgery/CSRF rejection to form consumers via IAntiforgeryValidationFeature#67082
DeagleGross merged 7 commits into
dotnet:mainfrom
DeagleGross:dmkorolev/csrf-in-blazor

Conversation

@DeagleGross

@DeagleGross DeagleGross commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

CsrfProtectionMiddleware no longer short-circuits a cross-origin request. Instead it records its verdict on IAntiforgeryValidationFeature — the same feature the token-based AntiforgeryMiddleware uses — 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 == false verdict does nothing on its own — the request is only rejected when a component reads the form (or otherwise inspects the feature):

  • Endpoint that reads the form (MVC action with antiforgery, minimal API [FromForm], Blazor SSR form POST): the invalid verdict is observed and the request is rejected with 400.
  • Endpoint that never touches the form: nothing reads the feature, so the request proceeds. This matches how token antiforgery already behaves — an unsafe request with a bad/missing token is not rejected unless the form is actually read.

Where the verdict is read and the request is rejected

Four consumers read IAntiforgeryValidationFeature and reject on IsValid == false. Each first confirms a middleware actually ran (an HttpContext.Items marker for Antiforgery or CSRF) before trusting the verdict:

  1. AntiforgeryMiddlewareAuthorizationFilter — MVC
  2. RequestDelegateFactory — minimal APIs binding [FromForm]
  3. FormFeature — backstop when the form is read directly
  4. RazorComponentEndpointInvoker — Blazor SSR

Token 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)

RazorComponentEndpointInvoker no longer calls IAntiforgery.ValidateRequestAsync itself. It trusts the verdict left by the upstream middleware and returns 400 only when IAntiforgeryValidationFeature.IsValid == false. It also skips antiforgery token generation when no token middleware ran. Apps which calls UseAntiforgery() see no change. Apps that removed UseAntiforgery() (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[&lt;AntiforgeryToken /&gt; · emits __RequestVerificationToken input]
Loading

Notes


Related #51981
Closes #67081

@DeagleGross
DeagleGross requested a review from javiercn June 8, 2026 15:41
@DeagleGross DeagleGross self-assigned this Jun 8, 2026
@DeagleGross
DeagleGross requested review from a team and halter73 as code owners June 8, 2026 15:41
Copilot AI review requested due to automatic review settings June 8, 2026 15:41
@DeagleGross DeagleGross added breaking-change This issue / pr will introduce a breaking change, when resolved / merged. area-blazor Includes: Blazor, Razor Components labels Jun 8, 2026
@dotnet-policy-service dotnet-policy-service Bot added the needs-breaking-change-announcement Indicates that breaking change announcement shuold be posted and linked to this PR label Jun 8, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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:
\n- [ ] Create an announcement issue by using the ASP.NET Core breaking change issue template.
\n- [ ] Link the breaking change announcement issue from this PR.
\n- [ ] Remove the needs-breaking-change-announcement label.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MiddlewareInvokedKeys and have Antiforgery/CSRF middleware set endpoint-invoked markers in HttpContext.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

Comment thread src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs Outdated
Comment thread src/Components/Endpoints/test/RazorComponentEndpointInvokerTest.cs
@javiercn

Copy link
Copy Markdown
Member

@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.

@DeagleGross

Copy link
Copy Markdown
Member Author

@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.

AntiforgeryToken already handles null tokens gracefully:

if (_requestToken != null)
{
_handle.Render(RenderField);
}

I've verified that having <AntiforgeryToken /> component on the page will be a no-op without any failure.

@DeagleGross
DeagleGross requested review from a team, BrennanConroy and tdykstra as code owners June 16, 2026 12:11
@DeagleGross DeagleGross changed the title [breaking] Razor Components: defer to upstream antiforgery / CSRF middleware [breaking] Defer antiforgery/CSRF rejection to form consumers via IAntiforgeryValidationFeature Jun 16, 2026
Co-authored-by: Copilot <copilot@github.com>
@DeagleGross
DeagleGross enabled auto-merge (squash) June 18, 2026 08:28
@wtgodbe
wtgodbe disabled auto-merge June 18, 2026 16:04
@DeagleGross

Copy link
Copy Markdown
Member Author

/ba-g unrelated: macOS quarantined-pr job timeouts

@DeagleGross
DeagleGross merged commit d54f274 into dotnet:main Jun 18, 2026
23 of 25 checks passed
@DeagleGross
DeagleGross deleted the dmkorolev/csrf-in-blazor branch June 18, 2026 16:12
DeagleGross pushed a commit to DeagleGross/aspnetcore that referenced this pull request Jun 18, 2026
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>
DeagleGross pushed a commit to DeagleGross/aspnetcore that referenced this pull request Jun 22, 2026
…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>
DeagleGross pushed a commit to DeagleGross/aspnetcore that referenced this pull request Jun 22, 2026
…#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>
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview6 milestone Jun 23, 2026
DeagleGross added a commit that referenced this pull request Jun 30, 2026
…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.
javiercn pushed a commit that referenced this pull request Jun 30, 2026
…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.
@pablopioli

Copy link
Copy Markdown

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.

@BrennanConroy

Copy link
Copy Markdown
Member

Please file an issue, we don't track comments on closed PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components breaking-change This issue / pr will introduce a breaking change, when resolved / merged. needs-breaking-change-announcement Indicates that breaking change announcement shuold be posted and linked to this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate CSRF usage in Blazor

7 participants