Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,94 @@ public async Task CsrfProtection_DisabledEndpointReExecutesIntoAntiforgeryRequir
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public async Task CsrfProtection_ImplicitRouting_WithReExecute_NormalRequestToAntiforgeryEndpoint_Succeeds()
{
// Regression test for https://github.com/dotnet/aspnetcore/issues/67628 (the Blazor Web template shape).
// UseStatusCodePagesWithReExecute builds a re-execution branch whose routing consumes the framework's
// single-use post-routing (auth/authz/CSRF) block. Before the fix, the re-execution branch stole that block
// from the primary pipeline, so a normal request to an antiforgery-required endpoint threw
// "a middleware was not found that supports anti-forgery" (HTTP 500).
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
using var app = builder.Build();

app.UseStatusCodePagesWithReExecute("/not-found");

app.MapGet("/", () => "home").WithMetadata(new RequireAntiforgeryTokenAttribute());
app.MapGet("/not-found", (HttpContext ctx) =>
{
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
return ctx.Response.WriteAsync("not found");
}).WithMetadata(new RequireAntiforgeryTokenAttribute());

await app.StartAsync();

var client = app.GetTestClient();
var response = await client.GetAsync("/");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("home", await response.Content.ReadAsStringAsync());
}

[Fact]
public async Task CsrfProtection_ExplicitRouting_WithReExecute_NormalRequestToAntiforgeryEndpoint_Succeeds()
{
// Companion to the implicit-routing regression test above: the same block-stealing bug also affects apps that
// call UseRouting() explicitly (the framework defers auth/authz/CSRF into the same single-use slot).
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
using var app = builder.Build();

app.UseRouting();
app.UseStatusCodePagesWithReExecute("/not-found");

app.MapGet("/", () => "home").WithMetadata(new RequireAntiforgeryTokenAttribute());
app.MapGet("/not-found", (HttpContext ctx) =>
{
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
return ctx.Response.WriteAsync("not found");
}).WithMetadata(new RequireAntiforgeryTokenAttribute());

await app.StartAsync();

var client = app.GetTestClient();
var response = await client.GetAsync("/");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("home", await response.Content.ReadAsStringAsync());
}

[Fact]
public async Task CsrfProtection_ImplicitRouting_WithReExecute_StillProtectsNormalCrossOriginPost()
{
// Ensures restoring the post-routing block for the primary pipeline does not weaken protection: a cross-origin
// POST to an antiforgery-required endpoint must still be rejected when re-execution is configured.
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
using var app = builder.Build();

app.UseStatusCodePagesWithReExecute("/not-found");

app.MapPost("/protected", EnforceCsrfProtected);
app.MapGet("/not-found", (HttpContext ctx) =>
{
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
return ctx.Response.WriteAsync("not found");
}).WithMetadata(new RequireAntiforgeryTokenAttribute());

await app.StartAsync();

var client = app.GetTestClient();
var request = new HttpRequestMessage(HttpMethod.Post, "/protected");
request.Headers.Add("Sec-Fetch-Site", "cross-site");

var response = await client.SendAsync(request);

Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal("protected", await response.Content.ReadAsStringAsync());
}

[Fact]
public async Task CsrfProtection_NotAutoInjected_WhenAppHasNoEndpointDataSources()
{
Expand Down
25 changes: 24 additions & 1 deletion src/Shared/Reroute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ internal static class RerouteHelper
internal const string GlobalRouteBuilderKey = "__GlobalEndpointRouteBuilder";
internal const string UseRoutingKey = "__UseRouting";

// Keep in sync with Microsoft.AspNetCore.Http.MiddlewareInvokedKeys.PostRoutingPipeline. It is duplicated as a
// literal here because this shared source file is compiled into assemblies that don't all include
// MiddlewareInvokedKeys.cs (matching how GlobalRouteBuilderKey/UseRoutingKey are duplicated above).
private const string PostRoutingPipelineKey = "__Internal_PostRoutingPipeline";

internal static RequestDelegate Reroute(IApplicationBuilder app, object routeBuilder, RequestDelegate next)
{
if (app.Properties.TryGetValue(UseRoutingKey, out var useRouting) && useRouting is Func<IApplicationBuilder, IApplicationBuilder> useRoutingFunc)
Expand All @@ -26,7 +31,25 @@ internal static RequestDelegate Reroute(IApplicationBuilder app, object routeBui
// apply the next middleware
builder.Run(next);

return builder.Build();
// Building this re-execution branch composes an EndpointRoutingMiddleware that consumes the framework's
// single-use post-routing middleware slot (the implicit authentication/authorization/CSRF pipeline added
// by WebApplicationBuilder) from the shared global route builder's properties. Capture it beforehand and
// restore it afterwards so the primary request pipeline's routing still runs that implicit middleware.
// Without this, a normal request to an antiforgery-required endpoint in an app that also re-executes
// (e.g. UseStatusCodePagesWithReExecute in the Blazor Web template) fails with
// "a middleware was not found that supports anti-forgery". See https://github.com/dotnet/aspnetcore/issues/67628.
var routeBuilderProperties = (routeBuilder as IApplicationBuilder)?.Properties;
object? postRoutingBlock = null;
routeBuilderProperties?.TryGetValue(PostRoutingPipelineKey, out postRoutingBlock);

var reroutePipeline = builder.Build();

if (postRoutingBlock is not null && routeBuilderProperties is not null)
{
routeBuilderProperties[PostRoutingPipelineKey] = postRoutingBlock;
}

return reroutePipeline;
}

return next;
Expand Down
Loading