Smarter antiforgery

Adds the concept of an IAntiforgeryPolicy marker interface as well as
the ability to overide policy with a 'closer' filter.

Adds a new [IgnoreAntiforgeryToken] attribute for overriding a scoped
antiforgery policy.

Adds a new [AutoValidateAntiforgeryToken] attribute (good name tbd) for
applying an application-wide antiforgery token. The idea is that you can
configure this as a global filter if your site is acting as a pure
browser-based or 1st party SPA. This new attribute only validates the
token for unsafe HTTP methods, so you can apply it broadly.
This commit is contained in:
Ryan Nowak 2015-12-17 14:33:23 -08:00
parent 0a2b6205c9
commit a500a93dfb
10 changed files with 358 additions and 39 deletions

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// An attribute that causes validation of antiforgery tokens for all unsafe HTTP methods. An antiforgery
/// token is required for HTTP methods other than GET, HEAD, OPTIONS, and TRACE.
/// </summary>
/// <remarks>
/// <see cref="AutoValidateAntiforgeryTokenAttribute"/> can be applied at as a global filter to trigger
/// validation of antiforgery tokens by default for an application. Use
/// <see cref="IgnoreAntiforgeryTokenAttribute"/> to suppress validation of the antiforgery token for
/// a controller or action.
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AutoValidateAntiforgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter
{
/// <inheritdoc />
public int Order { get; set; }
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<AutoValidateAntiforgeryTokenAuthorizationFilter>();
}
}
}

View File

@ -136,6 +136,12 @@ namespace Microsoft.Extensions.DependencyInjection
// This does caching so it should stay singleton
services.TryAddSingleton<ITempDataProvider, SessionStateTempDataProvider>();
//
// Antiforgery
//
services.TryAddSingleton<ValidateAntiforgeryTokenAuthorizationFilter>();
services.TryAddSingleton<AutoValidateAntiforgeryTokenAuthorizationFilter>();
// These are stateless so their lifetime isn't really important.
services.TryAddSingleton<ITempDataDictionaryFactory, TempDataDictionaryFactory>();
services.TryAddSingleton<SaveTempDataFilter>();

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ViewFeatures;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// An attribute which skips antiforgery token validation.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class IgnoreAntiforgeryTokenAttribute : Attribute, IAntiforgeryPolicy, IOrderedFilter
{
/// <inheritdoc />
public int Order { get; set; }
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Antiforgery;
using Microsoft.AspNet.Mvc.Filters;
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
{
public class AutoValidateAntiforgeryTokenAuthorizationFilter : ValidateAntiforgeryTokenAuthorizationFilter
{
public AutoValidateAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery)
: base(antiforgery)
{
}
protected override bool ShouldValidate(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var method = context.HttpContext.Request.Method;
if (string.Equals("GET", method, StringComparison.OrdinalIgnoreCase) ||
string.Equals("HEAD", method, StringComparison.OrdinalIgnoreCase) ||
string.Equals("TRACE", method, StringComparison.OrdinalIgnoreCase) ||
string.Equals("OPTIONS", method, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Anything else requires a token.
return true;
}
}
}

View File

@ -0,0 +1,69 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNet.Antiforgery;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.Internal;
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
{
public class ValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
{
private readonly IAntiforgery _antiforgery;
public ValidateAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery)
{
if (antiforgery == null)
{
throw new ArgumentNullException(nameof(antiforgery));
}
_antiforgery = antiforgery;
}
public Task OnAuthorizationAsync(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (IsClosestAntiforgeryPolicy(context.Filters) && ShouldValidate(context))
{
return _antiforgery.ValidateRequestAsync(context.HttpContext);
}
return TaskCache.CompletedTask;
}
protected virtual bool ShouldValidate(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return true;
}
private bool IsClosestAntiforgeryPolicy(IList<IFilterMetadata> filters)
{
// Determine if this instance is the 'effective' antiforgery policy.
for (var i = filters.Count - 1; i >= 0; i--)
{
var filter = filters[i];
if (filter is IAntiforgeryPolicy)
{
return object.ReferenceEquals(this, filter);
}
}
Debug.Fail("The current instance should be in the list of filters.");
return false;
}
}
}

View File

@ -2,9 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Antiforgery;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNet.Mvc
@ -24,8 +23,7 @@ namespace Microsoft.AspNet.Mvc
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
var antiforgery = serviceProvider.GetRequiredService<IAntiforgery>();
return new ValidateAntiforgeryTokenAuthorizationFilter(antiforgery);
return serviceProvider.GetRequiredService<ValidateAntiforgeryTokenAuthorizationFilter>();
}
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc.Filters;
namespace Microsoft.AspNet.Mvc.ViewFeatures
{
/// <summary>
/// A marker interface for filters which define a policy for antiforgery token validation.
/// </summary>
public interface IAntiforgeryPolicy : IFilterMetadata
{
}
}

View File

@ -1,35 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Antiforgery;
using Microsoft.AspNet.Mvc.Filters;
namespace Microsoft.AspNet.Mvc.ViewFeatures
{
public class ValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter
{
private readonly IAntiforgery _antiforgery;
public ValidateAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery)
{
if (antiforgery == null)
{
throw new ArgumentNullException(nameof(antiforgery));
}
_antiforgery = antiforgery;
}
public Task OnAuthorizationAsync(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return _antiforgery.ValidateRequestAsync(context.HttpContext);
}
}
}

View File

@ -0,0 +1,101 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.Antiforgery;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
{
public class AutoValidateAntiforgeryTokenAuthorizationFilterTest
{
[Theory]
[InlineData("PUT")]
[InlineData("POsT")]
[InlineData("DeLETE")]
public async Task Filter_ValidatesAntiforgery_ForUnsafeMethod(string httpMethod)
{
// Arrange
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
antiforgery
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
.Returns(Task.FromResult(0))
.Verifiable();
var filter = new AutoValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
actionContext.HttpContext.Request.Method = httpMethod;
var context = new AuthorizationContext(actionContext, new[] { filter });
// Act
await filter.OnAuthorizationAsync(context);
// Assert
antiforgery.Verify();
}
[Theory]
[InlineData("GET")]
[InlineData("HEAD")]
[InlineData("TracE")]
[InlineData("OPTIONs")]
public async Task Filter_SkipsAntiforgeryVerification_ForSafeMethod(string httpMethod)
{
// Arrange
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
antiforgery
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
.Returns(Task.FromResult(0))
.Verifiable();
var filter = new AutoValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
actionContext.HttpContext.Request.Method = httpMethod;
var context = new AuthorizationContext(actionContext, new[] { filter });
// Act
await filter.OnAuthorizationAsync(context);
// Assert
antiforgery.Verify(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()), Times.Never());
}
[Fact]
public async Task Filter_SkipsAntiforgeryVerification_WhenOverridden()
{
// Arrange
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
antiforgery
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
.Returns(Task.FromResult(0))
.Verifiable();
var filter = new AutoValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
actionContext.HttpContext.Request.Method = "POST";
var context = new AuthorizationContext(actionContext, new IFilterMetadata[]
{
filter,
new IgnoreAntiforgeryTokenAttribute(),
});
// Act
await filter.OnAuthorizationAsync(context);
// Assert
antiforgery.Verify(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()), Times.Never());
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.Antiforgery;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
{
public class ValidateAntiforgeryTokenAuthorizationFilterTest
{
[Theory]
[InlineData("PUT")]
[InlineData("POsT")]
[InlineData("DeLETE")]
[InlineData("GET")]
[InlineData("HEAD")]
[InlineData("TracE")]
[InlineData("OPTIONs")]
public async Task Filter_ValidatesAntiforgery_ForAllMethods(string httpMethod)
{
// Arrange
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
antiforgery
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
.Returns(Task.FromResult(0))
.Verifiable();
var filter = new ValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
actionContext.HttpContext.Request.Method = httpMethod;
var context = new AuthorizationContext(actionContext, new[] { filter });
// Act
await filter.OnAuthorizationAsync(context);
// Assert
antiforgery.Verify();
}
[Fact]
public async Task Filter_SkipsAntiforgeryVerification_WhenOverridden()
{
// Arrange
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
antiforgery
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
.Returns(Task.FromResult(0))
.Verifiable();
var filter = new ValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object);
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
actionContext.HttpContext.Request.Method = "POST";
var context = new AuthorizationContext(actionContext, new IFilterMetadata[]
{
filter,
new IgnoreAntiforgeryTokenAttribute(),
});
// Act
await filter.OnAuthorizationAsync(context);
// Assert
antiforgery.Verify(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()), Times.Never());
}
}
}