Skip to content

Commit 78bec18

Browse files
authored
[Blazor] Add support for antiforgery (#49108)
* Adds a new IAntiforgeryMetadata interface to describe the antiforgery requirement. * Adds a new RequireAntiforgeryToken attribute to require antiforgery. * Adds RequireAntiforgeryToken to all razor component endpoints. * Adds a new AntiforgeryTokenStateProvider service that retrieves and renders the antiforgery token for the app. * Adds a new AntiforgeryToken component that renders the request antiforgery token as a hidden field. * Makes EditForm automatically render the AntiforgeryToken when inside of a form binding context.
1 parent 24fdfa9 commit 78bec18

File tree

44 files changed

+722
-46
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+722
-46
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Antiforgery.Infrastructure;
5+
6+
/// <summary>
7+
/// A marker interface which can be used to identify antiforgery metadata.
8+
/// </summary>
9+
public interface IAntiforgeryMetadata
10+
{
11+
/// <summary>
12+
/// Gets a value indicating whether the antiforgery token should be validated.
13+
/// </summary>
14+
bool Required { get; }
15+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Antiforgery.Infrastructure.IAntiforgeryMetadata
3+
Microsoft.AspNetCore.Antiforgery.Infrastructure.IAntiforgeryMetadata.Required.get -> bool
4+
Microsoft.AspNetCore.Antiforgery.RequireAntiforgeryTokenAttribute
5+
Microsoft.AspNetCore.Antiforgery.RequireAntiforgeryTokenAttribute.RequireAntiforgeryTokenAttribute(bool required = true) -> void
6+
Microsoft.AspNetCore.Antiforgery.RequireAntiforgeryTokenAttribute.Required.get -> bool
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Antiforgery.Infrastructure;
5+
6+
namespace Microsoft.AspNetCore.Antiforgery;
7+
8+
/// <summary>
9+
/// An attribute that can be used to indicate whether the antiforgery token must be validated.
10+
/// </summary>
11+
/// <param name="required">A value indicating whether the antiforgery token should be validated.</param>
12+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
13+
public class RequireAntiforgeryTokenAttribute(bool required = true) : Attribute, IAntiforgeryMetadata
14+
{
15+
/// <summary>
16+
/// Gets or sets a value indicating whether the antiforgery token should be validated.
17+
/// </summary>
18+
/// <remarks>
19+
/// Defaults to <see langword="true"/>; <see langword="false"/> indicates that
20+
/// the validation check for the antiforgery token can be avoided.
21+
/// </remarks>
22+
public bool Required { get; } = required;
23+
}

src/Components/Components/src/Rendering/RenderTreeBuilder.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ public void AddAttribute(int sequence, string name)
175175
throw new InvalidOperationException($"Valueless attributes may only be added immediately after frames of type {RenderTreeFrameType.Element}");
176176
}
177177

178+
if (TrackNamedEventHandlers && string.Equals(name, "@onsubmit:name", StringComparison.Ordinal))
179+
{
180+
_entries.AppendAttribute(sequence, name, "");
181+
}
182+
178183
_entries.AppendAttribute(sequence, name, BoxedTrue);
179184
}
180185

@@ -226,7 +231,14 @@ public void AddAttribute(int sequence, string name, string? value)
226231
AssertCanAddAttribute();
227232
if (value != null || _lastNonAttributeFrameType == RenderTreeFrameType.Component)
228233
{
229-
_entries.AppendAttribute(sequence, name, value);
234+
if (TrackNamedEventHandlers && value != null && string.Equals(name, "@onsubmit:name", StringComparison.Ordinal))
235+
{
236+
SetEventHandlerName(value);
237+
}
238+
else
239+
{
240+
_entries.AppendAttribute(sequence, name, value);
241+
}
230242
}
231243
else
232244
{
@@ -371,6 +383,11 @@ public void AddAttribute(int sequence, string name, object? value)
371383
{
372384
if (boolValue)
373385
{
386+
if (TrackNamedEventHandlers && string.Equals(name, "@onsubmit:name", StringComparison.Ordinal))
387+
{
388+
_entries.AppendAttribute(sequence, name, value);
389+
}
390+
374391
_entries.AppendAttribute(sequence, name, BoxedTrue);
375392
}
376393
else
@@ -396,8 +413,17 @@ public void AddAttribute(int sequence, string name, object? value)
396413
}
397414
else
398415
{
399-
// The value is either a string, or should be treated as a string.
400-
_entries.AppendAttribute(sequence, name, value.ToString());
416+
var valueAsString = value.ToString();
417+
if (TrackNamedEventHandlers && valueAsString != null && string.Equals(name, "@onsubmit:name", StringComparison.Ordinal))
418+
{
419+
SetEventHandlerName(valueAsString);
420+
}
421+
else
422+
{
423+
// The value is either a string, or should be treated as a string.
424+
_entries.AppendAttribute(sequence, name, valueAsString);
425+
}
426+
401427
}
402428
}
403429
else if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
@@ -873,6 +899,18 @@ internal void ProcessDuplicateAttributes(int first)
873899
// This attribute has been overridden. For now, blank out its name to *mark* it. We'll do a pass
874900
// later to wipe it out.
875901
frame = default;
902+
// We are wiping out this frame, which means that if we are tracking named events, we have to adjust the
903+
// indexes of the named event handlers that come after this frame.
904+
if (_seenEventHandlerNames != null && _seenEventHandlerNames.Count > 0)
905+
{
906+
foreach (var (name, eventIndex) in _seenEventHandlerNames)
907+
{
908+
if (eventIndex >= i)
909+
{
910+
_seenEventHandlerNames[name] = eventIndex - 1;
911+
}
912+
}
913+
}
876914
}
877915
else
878916
{

src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.Antiforgery;
45
using Microsoft.AspNetCore.Builder;
56
using Microsoft.AspNetCore.Components.Discovery;
67
using Microsoft.AspNetCore.Http;
@@ -30,6 +31,9 @@ internal void AddEndpoints(
3031
RoutePatternFactory.Parse(pageDefinition.Route),
3132
order: 0);
3233

34+
// Require antiforgery by default, let the page override it.
35+
builder.Metadata.Add(new RequireAntiforgeryTokenAttribute());
36+
3337
// All attributes defined for the type are included as metadata.
3438
foreach (var attribute in pageDefinition.Metadata)
3539
{

src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.AspNetCore.Components.Binding;
77
using Microsoft.AspNetCore.Components.Endpoints;
88
using Microsoft.AspNetCore.Components.Endpoints.DependencyInjection;
9+
using Microsoft.AspNetCore.Components.Endpoints.Forms;
910
using Microsoft.AspNetCore.Components.Forms;
1011
using Microsoft.AspNetCore.Components.Infrastructure;
1112
using Microsoft.AspNetCore.Components.Routing;
@@ -30,6 +31,9 @@ public static class RazorComponentsServiceCollectionExtensions
3031
[RequiresUnreferencedCode("Razor Components does not currently support native AOT.", Url = "https://aka.ms/aspnet/nativeaot")]
3132
public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services)
3233
{
34+
// Dependencies
35+
services.AddAntiforgery();
36+
3337
services.TryAddSingleton<RazorComponentsMarkerService>();
3438

3539
// Results
@@ -60,7 +64,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
6064
services.TryAddScoped<IFormValueSupplier, DefaultFormValuesSupplier>();
6165
services.TryAddEnumerable(ServiceDescriptor.Scoped<CascadingModelBindingProvider, CascadingQueryModelBindingProvider>());
6266
services.TryAddEnumerable(ServiceDescriptor.Scoped<CascadingModelBindingProvider, CascadingFormModelBindingProvider>());
63-
67+
services.TryAddScoped<AntiforgeryStateProvider, EndpointAntiforgeryStateProvider>();
6468
return new DefaultRazorComponentsBuilder(services);
6569
}
6670

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Antiforgery;
5+
using Microsoft.AspNetCore.Components.Forms;
6+
using Microsoft.AspNetCore.Http;
7+
8+
namespace Microsoft.AspNetCore.Components.Endpoints.Forms;
9+
10+
internal class EndpointAntiforgeryStateProvider(IAntiforgery antiforgery, PersistentComponentState state) : DefaultAntiforgeryStateProvider(state)
11+
{
12+
private HttpContext? _context;
13+
14+
internal void SetRequestContext(HttpContext context)
15+
{
16+
_context = context;
17+
}
18+
19+
public override AntiforgeryRequestToken? GetAntiforgeryToken()
20+
{
21+
if (_context == null)
22+
{
23+
return null;
24+
}
25+
26+
// We already have a callback setup to generate the token when the response starts if needed.
27+
// If we need the tokens before we start streaming the response, we'll generate and store them;
28+
// otherwise we'll just retrieve them.
29+
// In case there are no tokens available, we are going to return null and no-op.
30+
var tokens = !_context.Response.HasStarted ? antiforgery.GetAndStoreTokens(_context) : antiforgery.GetTokens(_context);
31+
if (tokens.RequestToken is null)
32+
{
33+
return null;
34+
}
35+
36+
return new AntiforgeryRequestToken(tokens.RequestToken, tokens.FormFieldName);
37+
}
38+
}

src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<Compile Include="$(RepoRoot)src\Shared\Components\ProtectedPrerenderComponentApplicationStore.cs" LinkBase="DependencyInjection" />
3636
<Compile Include="$(RepoRoot)src\Shared\ClosedGenericMatcher\ClosedGenericMatcher.cs" LinkBase="Binding" />
3737
<Compile Include="$(ComponentsSharedSourceRoot)src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
38+
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultAntiforgeryStateProvider.cs" LinkBase="Forms" />
3839

3940
<Compile Include="$(SharedSourceRoot)PropertyHelper\**\*.cs" />
4041

@@ -44,6 +45,7 @@
4445
</ItemGroup>
4546

4647
<ItemGroup>
48+
<Reference Include="Microsoft.AspNetCore.Antiforgery" />
4749
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
4850
<Reference Include="Microsoft.AspNetCore.Components.Web" />
4951
<Reference Include="Microsoft.AspNetCore.DataProtection.Extensions" />

src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Buffers;
5+
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
67
using System.Text;
78
using System.Text.Encodings.Web;
9+
using Microsoft.AspNetCore.Antiforgery;
10+
using Microsoft.AspNetCore.Antiforgery.Infrastructure;
811
using Microsoft.AspNetCore.Http;
912
using Microsoft.AspNetCore.WebUtilities;
1013
using Microsoft.Extensions.DependencyInjection;
@@ -36,13 +39,27 @@ private async Task RenderComponentCore()
3639
_context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType;
3740
_renderer.InitializeStreamingRenderingFraming(_context);
3841

39-
if (!await TryValidateRequestAsync(out var isPost, out var handler))
42+
// Metadata controls whether we require antiforgery protection for this endpoint or we should skip it.
43+
// The default for razor component endpoints is to require the metadata, but it can be overriden by
44+
// the developer.
45+
var antiforgeryMetadata = _context.GetEndpoint()!.Metadata.GetMetadata<IAntiforgeryMetadata>();
46+
var antiforgery = _context.RequestServices.GetRequiredService<IAntiforgery>();
47+
var (valid, isPost, handler) = await ValidateRequestAsync(antiforgeryMetadata?.Required == true ? antiforgery : null);
48+
if (!valid)
4049
{
4150
// If the request is not valid we've already set the response to a 400 or similar
4251
// and we can just exit early.
4352
return;
4453
}
4554

55+
_context.Response.OnStarting(() =>
56+
{
57+
// Generate the antiforgery tokens before we start streaming the response, as it needs
58+
// to set the cookie header.
59+
antiforgery!.GetAndStoreTokens(_context);
60+
return Task.CompletedTask;
61+
});
62+
4663
await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync(
4764
_context,
4865
componentType: _componentType,
@@ -89,16 +106,21 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync(
89106
await writer.FlushAsync();
90107
}
91108

92-
private Task<bool> TryValidateRequestAsync(out bool isPost, out string? handler)
109+
private async Task<RequestValidationState> ValidateRequestAsync(IAntiforgery? antiforgery)
93110
{
94-
handler = null;
95-
isPost = HttpMethods.IsPost(_context.Request.Method);
111+
var isPost = HttpMethods.IsPost(_context.Request.Method);
96112
if (isPost)
97113
{
98-
return Task.FromResult(TrySetFormHandler(out handler));
114+
var valid = antiforgery == null || await antiforgery.IsRequestValidAsync(_context);
115+
if (!valid)
116+
{
117+
_context.Response.StatusCode = StatusCodes.Status400BadRequest;
118+
}
119+
var formValid = TrySetFormHandler(out var handler);
120+
return new(valid && formValid, isPost, handler);
99121
}
100122

101-
return Task.FromResult(true);
123+
return new(true, false, null);
102124
}
103125

104126
private bool TrySetFormHandler([NotNullWhen(true)] out string? handler)
@@ -128,4 +150,26 @@ private static TextWriter CreateResponseWriter(Stream bodyStream)
128150
const int DefaultBufferSize = 16 * 1024;
129151
return new HttpResponseStreamWriter(bodyStream, Encoding.UTF8, DefaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared);
130152
}
153+
154+
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
155+
private readonly struct RequestValidationState(bool isValid, bool isPost, string? handlerName)
156+
{
157+
public bool IsValid => isValid;
158+
159+
public bool IsPost => isPost;
160+
161+
public string? HandlerName => handlerName;
162+
163+
private string GetDebuggerDisplay()
164+
{
165+
return $"{nameof(RequestValidationState)}: {IsValid} {IsPost} {HandlerName}";
166+
}
167+
168+
public void Deconstruct(out bool isValid, out bool isPost, out string? handlerName)
169+
{
170+
isValid = IsValid;
171+
isPost = IsPost;
172+
handlerName = HandlerName;
173+
}
174+
}
131175
}

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Text;
77
using Microsoft.AspNetCore.Components.Authorization;
88
using Microsoft.AspNetCore.Components.Endpoints.DependencyInjection;
9+
using Microsoft.AspNetCore.Components.Endpoints.Forms;
910
using Microsoft.AspNetCore.Components.Forms;
1011
using Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure;
1112
using Microsoft.AspNetCore.Components.Infrastructure;
@@ -75,19 +76,24 @@ internal static async Task InitializeStandardComponentServicesAsync(
7576
var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService<NavigationManager>();
7677
navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request));
7778

78-
var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
79-
if (authenticationStateProvider != null)
79+
if (httpContext.RequestServices.GetService<AuthenticationStateProvider>() is IHostEnvironmentAuthenticationStateProvider authenticationStateProvider)
8080
{
8181
var authenticationState = new AuthenticationState(httpContext.User);
8282
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
8383
}
8484

85-
var formData = httpContext.RequestServices.GetRequiredService<FormDataProvider>() as IHostEnvironmentFormDataProvider;
86-
if (handler != null && form != null && formData != null)
85+
if (handler != null &&
86+
form != null &&
87+
httpContext.RequestServices.GetRequiredService<FormDataProvider>() is IHostEnvironmentFormDataProvider formData)
8788
{
8889
formData.SetFormData(handler, new FormCollectionReadOnlyDictionary(form));
8990
}
9091

92+
if (httpContext.RequestServices.GetService<AntiforgeryStateProvider>() is EndpointAntiforgeryStateProvider antiforgery)
93+
{
94+
antiforgery.SetRequestContext(httpContext);
95+
}
96+
9197
// It's important that this is initialized since a component might try to restore state during prerendering
9298
// (which will obviously not work, but should not fail)
9399
var componentApplicationLifetime = httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();

src/Components/Endpoints/test/EndpointHtmlRendererTest.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text.Encodings.Web;
55
using System.Text.Json;
66
using System.Text.RegularExpressions;
7+
using Microsoft.AspNetCore.Components.Endpoints.Forms;
78
using Microsoft.AspNetCore.Components.Endpoints.Tests.TestComponents;
89
using Microsoft.AspNetCore.Components.Forms;
910
using Microsoft.AspNetCore.Components.Infrastructure;
@@ -1297,6 +1298,11 @@ private static ServiceCollection CreateDefaultServiceCollection()
12971298
services.AddSingleton(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State);
12981299
services.AddSingleton<ServerComponentSerializer>();
12991300
services.AddSingleton<FormDataProvider, HttpContextFormDataProvider>();
1301+
services.AddAntiforgery();
1302+
services.AddSingleton<ComponentStatePersistenceManager>();
1303+
services.AddSingleton<PersistentComponentState>(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State);
1304+
services.AddSingleton<AntiforgeryStateProvider, EndpointAntiforgeryStateProvider>();
1305+
13001306
return services;
13011307
}
13021308

0 commit comments

Comments
 (0)