-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Antiforgery middleware #38314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Antiforgery middleware #38314
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Http.Abstractions.Metadata; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Microsoft.AspNetCore.Antiforgery; | ||
|
||
internal sealed partial class AntiforgeryMiddleware | ||
{ | ||
private readonly IAntiforgery _antiforgery; | ||
private readonly RequestDelegate _next; | ||
private readonly ILogger<AntiforgeryMiddleware> _logger; | ||
|
||
public AntiforgeryMiddleware(IAntiforgery antiforgery, RequestDelegate next, ILogger<AntiforgeryMiddleware> logger) | ||
{ | ||
_antiforgery = antiforgery; | ||
_next = next; | ||
_logger = logger; | ||
} | ||
|
||
public Task Invoke(HttpContext context) | ||
{ | ||
var endpoint = context.GetEndpoint(); | ||
if (endpoint is null) | ||
{ | ||
return _next(context); | ||
} | ||
|
||
var antiforgeryMetadata = endpoint.Metadata.GetMetadata<IAntiforgeryMetadata>(); | ||
if (antiforgeryMetadata is null) | ||
{ | ||
Log.NoAntiforgeryMetadataFound(_logger); | ||
return _next(context); | ||
} | ||
|
||
if (antiforgeryMetadata is not IValidateAntiforgeryMetadata validateAntiforgeryMetadata) | ||
{ | ||
Log.IgnoreAntiforgeryMetadataFound(_logger); | ||
return _next(context); | ||
} | ||
|
||
if (_antiforgery is DefaultAntiforgery defaultAntiforgery) | ||
{ | ||
var valueTask = defaultAntiforgery.TryValidateAsync(context, validateAntiforgeryMetadata.ValidateIdempotentRequests); | ||
if (valueTask.IsCompletedSuccessfully) | ||
{ | ||
var (success, message) = valueTask.GetAwaiter().GetResult(); | ||
if (success) | ||
{ | ||
Log.AntiforgeryValidationSucceeded(_logger); | ||
return _next(context); | ||
} | ||
else | ||
{ | ||
Log.AntiforgeryValidationFailed(_logger, message); | ||
return WriteAntiforgeryInvalidResponseAsync(context, message); | ||
} | ||
} | ||
|
||
return TryValidateAsyncAwaited(context, valueTask); | ||
} | ||
else | ||
{ | ||
return ValidateNonDefaultAntiforgery(context); | ||
} | ||
} | ||
|
||
private async Task TryValidateAsyncAwaited(HttpContext context, ValueTask<(bool success, string? message)> tryValidateTask) | ||
{ | ||
var (success, message) = await tryValidateTask; | ||
if (success) | ||
{ | ||
Log.AntiforgeryValidationSucceeded(_logger); | ||
await _next(context); | ||
} | ||
else | ||
{ | ||
Log.AntiforgeryValidationFailed(_logger, message); | ||
await context.Response.WriteAsJsonAsync(new ProblemDetails | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason this doesn't set the status code to 400 or call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops |
||
{ | ||
Status = StatusCodes.Status400BadRequest, | ||
Title = "Antiforgery validation failed", | ||
Detail = message, | ||
}); | ||
} | ||
} | ||
|
||
private async Task ValidateNonDefaultAntiforgery(HttpContext context) | ||
{ | ||
if (await _antiforgery.IsRequestValidAsync(context)) | ||
{ | ||
Log.AntiforgeryValidationSucceeded(_logger); | ||
await _next(context); | ||
} | ||
else | ||
{ | ||
Log.AntiforgeryValidationFailed(_logger, message: null); | ||
await WriteAntiforgeryInvalidResponseAsync(context, message: null); | ||
} | ||
} | ||
|
||
private static Task WriteAntiforgeryInvalidResponseAsync(HttpContext context, string? message) | ||
|
||
{ | ||
context.Response.StatusCode = StatusCodes.Status400BadRequest; | ||
return context.Response.WriteAsJsonAsync(new ProblemDetails | ||
{ | ||
Status = StatusCodes.Status400BadRequest, | ||
Title = "Antiforgery validation failed", | ||
Detail = message, | ||
}); | ||
} | ||
|
||
private static partial class Log | ||
{ | ||
[LoggerMessage(1, LogLevel.Debug, "No antiforgery metadata found on the endpoint.", EventName = "NoAntiforgeryMetadataFound")] | ||
public static partial void NoAntiforgeryMetadataFound(ILogger logger); | ||
|
||
[LoggerMessage(2, LogLevel.Debug, $"Antiforgery validation suppressed on endpoint because {nameof(IValidateAntiforgeryMetadata)} was not found.", EventName = "IgnoreAntiforgeryMetadataFound")] | ||
public static partial void IgnoreAntiforgeryMetadataFound(ILogger logger); | ||
|
||
[LoggerMessage(3, LogLevel.Debug, "Antiforgery validation completed successfully.", EventName = "AntiforgeryValidationSucceeded")] | ||
public static partial void AntiforgeryValidationSucceeded(ILogger logger); | ||
|
||
[LoggerMessage(4, LogLevel.Debug, "Antiforgery validation failed with message '{message}'.", EventName = "AntiforgeryValidationFailed")] | ||
public static partial void AntiforgeryValidationFailed(ILogger logger, string? message); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using Microsoft.AspNetCore.Antiforgery; | ||
using Microsoft.AspNetCore.Cors.Infrastructure; | ||
|
||
namespace Microsoft.AspNetCore.Builder; | ||
|
||
/// <summary> | ||
/// The <see cref="IApplicationBuilder"/> extensions for adding Antiforgery middleware support. | ||
/// </summary> | ||
public static class AntiforgeryMiddlewareExtensions | ||
{ | ||
/// <summary> | ||
/// Adds the Antiforgery middleware to the middleware pipeline. | ||
/// </summary> | ||
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param> | ||
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns> | ||
public static IApplicationBuilder UseAntiforgery(this IApplicationBuilder app) | ||
=> app.UseMiddleware<AntiforgeryMiddleware>(); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,10 +1,8 @@ | ||||||||||||||||||||||||||||||||||
// Licensed to the .NET Foundation under one or more agreements. | ||||||||||||||||||||||||||||||||||
// The .NET Foundation licenses this file to you under the MIT license. | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
using System; | ||||||||||||||||||||||||||||||||||
using System.Diagnostics; | ||||||||||||||||||||||||||||||||||
using System.Diagnostics.CodeAnalysis; | ||||||||||||||||||||||||||||||||||
using System.Threading.Tasks; | ||||||||||||||||||||||||||||||||||
using Microsoft.AspNetCore.Http; | ||||||||||||||||||||||||||||||||||
using Microsoft.Extensions.Logging; | ||||||||||||||||||||||||||||||||||
using Microsoft.Extensions.Options; | ||||||||||||||||||||||||||||||||||
|
@@ -100,35 +98,43 @@ public async Task<bool> IsRequestValidAsync(HttpContext httpContext) | |||||||||||||||||||||||||||||||||
throw new ArgumentNullException(nameof(httpContext)); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
var (result, _) = await TryValidateAsync(httpContext, validateIdempotentRequests: false); | ||||||||||||||||||||||||||||||||||
return result; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
internal async ValueTask<(bool success, string? errorMessage)> TryValidateAsync(HttpContext httpContext, bool validateIdempotentRequests) | ||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||
CheckSSLConfig(httpContext); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
var method = httpContext.Request.Method; | ||||||||||||||||||||||||||||||||||
if (HttpMethods.IsGet(method) || | ||||||||||||||||||||||||||||||||||
if ( | ||||||||||||||||||||||||||||||||||
!validateIdempotentRequests && | ||||||||||||||||||||||||||||||||||
(HttpMethods.IsGet(method) || | ||||||||||||||||||||||||||||||||||
HttpMethods.IsHead(method) || | ||||||||||||||||||||||||||||||||||
HttpMethods.IsOptions(method) || | ||||||||||||||||||||||||||||||||||
HttpMethods.IsTrace(method)) | ||||||||||||||||||||||||||||||||||
HttpMethods.IsTrace(method))) | ||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||
// Validation not needed for these request types. | ||||||||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||||||||
return (true, null); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
var tokens = await _tokenStore.GetRequestTokensAsync(httpContext); | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something should catch the AntiforgeryValidationException and convert it to a 400.
|
||||||||||||||||||||||||||||||||||
if (tokens.CookieToken == null) | ||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||
_logger.MissingCookieToken(_options.Cookie.Name); | ||||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||||
return (false, "Missing cookie token"); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
if (tokens.RequestToken == null) | ||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||
_logger.MissingRequestToken(_options.FormFieldName, _options.HeaderName); | ||||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||||
return (false, "Antiforgery token could not be found in the HTTP request."); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// Extract cookie & request tokens | ||||||||||||||||||||||||||||||||||
if (!TryDeserializeTokens(httpContext, tokens, out var deserializedCookieToken, out var deserializedRequestToken)) | ||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||||
return (false, "Unable to deserialize antiforgery tokens"); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// Validate | ||||||||||||||||||||||||||||||||||
|
@@ -147,7 +153,7 @@ public async Task<bool> IsRequestValidAsync(HttpContext httpContext) | |||||||||||||||||||||||||||||||||
_logger.ValidationFailed(message!); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return result; | ||||||||||||||||||||||||||||||||||
return (result, message); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/// <inheritdoc /> | ||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
#nullable enable | ||
Microsoft.AspNetCore.Builder.AntiforgeryMiddlewareExtensions | ||
static Microsoft.AspNetCore.Builder.AntiforgeryMiddlewareExtensions.UseAntiforgery(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace Microsoft.AspNetCore.Http.Abstractions.Metadata; | ||
|
||
/// <summary> | ||
/// A marker interface which can be used to identify Antiforgery metadata. | ||
/// </summary> | ||
public interface IAntiforgeryMetadata | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace Microsoft.AspNetCore.Http.Abstractions.Metadata; | ||
|
||
/// <summary> | ||
/// A marker interface which can be used to identify a resource with Antiforgery validation enabled. | ||
/// </summary> | ||
public interface IValidateAntiforgeryMetadata : IAntiforgeryMetadata | ||
{ | ||
/// <summary> | ||
/// Gets a value that determines if idempotent HTTP methods (<c>GET</c>, <c>HEAD</c>, <c>OPTIONS</c> and <c>TRACE</c>) are validated. | ||
/// </summary> | ||
bool ValidateIdempotentRequests { get; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
#nullable enable | ||
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string! | ||
Microsoft.AspNetCore.Http.Abstractions.Metadata.IAntiforgeryMetadata | ||
Microsoft.AspNetCore.Http.Abstractions.Metadata.IValidateAntiforgeryMetadata | ||
Microsoft.AspNetCore.Http.Abstractions.Metadata.IValidateAntiforgeryMetadata.ValidateIdempotentRequests.get -> bool | ||
abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Linq; | ||
using Microsoft.AspNetCore.Http.Abstractions.Metadata; | ||
using Microsoft.AspNetCore.Mvc.Filters; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels; | ||
|
||
/// <summary> | ||
/// An <see cref="IApplicationModelProvider"/> that removes antiforgery filters that appears as endpoint metadata. | ||
/// </summary> | ||
internal sealed class AntiforgeryApplicationModelProvider : IApplicationModelProvider | ||
{ | ||
private readonly MvcOptions _mvcOptions; | ||
|
||
public AntiforgeryApplicationModelProvider(IOptions<MvcOptions> mvcOptions) | ||
{ | ||
_mvcOptions = mvcOptions.Value; | ||
} | ||
|
||
// Run late in the pipeline so that we can pick up user configured AntiforgeryTokens. | ||
public int Order { get; } = 1000; | ||
|
||
public void OnProvidersExecuted(ApplicationModelProviderContext context) | ||
{ | ||
} | ||
|
||
public void OnProvidersExecuting(ApplicationModelProviderContext context) | ||
{ | ||
if (!_mvcOptions.EnableEndpointRouting) | ||
{ | ||
return; | ||
} | ||
|
||
foreach (var controller in context.Result.Controllers) | ||
{ | ||
RemoveAntiforgeryFilters(controller.Filters, controller.Selectors); | ||
|
||
foreach (var action in controller.Actions) | ||
{ | ||
RemoveAntiforgeryFilters(action.Filters, action.Selectors); | ||
} | ||
} | ||
} | ||
|
||
private static void RemoveAntiforgeryFilters(IList<IFilterMetadata> filters, IList<SelectorModel> selectorModels) | ||
{ | ||
for (var i = filters.Count - 1; i >= 0; i--) | ||
{ | ||
if (filters[i] is IAntiforgeryMetadata antiforgeryMetadata && | ||
selectorModels.All(s => s.EndpointMetadata.Contains(antiforgeryMetadata))) | ||
{ | ||
filters.RemoveAt(i); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this middleware important enough to be included in security check in
EndpointMiddleware
?aspnetcore/src/Http/Routing/src/EndpointMiddleware.cs
Lines 38 to 42 in bc118a3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that's on my list of things to do. We also need to add a StartupAnalyzer that suggests ordering this after Routing and AuthZ middlewares.