[Fixes #105] Disable caching when response uses antiforgery
This commit is contained in:
parent
08cb67b7e4
commit
3fc090e2fe
|
|
@ -14,7 +14,8 @@ namespace Microsoft.AspNetCore.Antiforgery
|
|||
{
|
||||
/// <summary>
|
||||
/// Generates an <see cref="AntiforgeryTokenSet"/> for this request and stores the cookie token
|
||||
/// in the response.
|
||||
/// in the response. This operation also sets the "Cache-control" and "Pragma" headers to "no-cache" and
|
||||
/// the "X-Frame-Options" header to "SAMEORIGIN".
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
|
||||
/// <returns>An <see cref="AntiforgeryTokenSet" /> with tokens for the response.</returns>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
private static readonly Action<ILogger, Exception> _newCookieToken;
|
||||
private static readonly Action<ILogger, Exception> _reusedCookieToken;
|
||||
private static readonly Action<ILogger, Exception> _tokenDeserializeException;
|
||||
private static readonly Action<ILogger, Exception> _responseCacheHeadersOverridenToNoCache;
|
||||
|
||||
static AntiforgeryLoggerExtensions()
|
||||
{
|
||||
|
|
@ -47,6 +48,11 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
LogLevel.Error,
|
||||
7,
|
||||
"An exception was thrown while deserializing the token.");
|
||||
_responseCacheHeadersOverridenToNoCache = LoggerMessage.Define(
|
||||
LogLevel.Warning,
|
||||
8,
|
||||
"The 'Cache-Control' and 'Pragma' headers have been overridden and set to 'no-cache' to prevent " +
|
||||
"caching of this response. Any response that uses antiforgery should not be cached.");
|
||||
}
|
||||
|
||||
public static void ValidationFailed(this ILogger logger, string message)
|
||||
|
|
@ -83,5 +89,10 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
{
|
||||
_tokenDeserializeException(logger, exception);
|
||||
}
|
||||
|
||||
public static void ResponseCacheHeadersOverridenToNoCache(this ILogger logger)
|
||||
{
|
||||
_responseCacheHeadersOverridenToNoCache(logger, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Antiforgery.Internal
|
||||
{
|
||||
|
|
@ -66,6 +67,10 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
}
|
||||
}
|
||||
|
||||
// Explicitly set the cache headers to 'no-cache'. This could override any user set value but this is fine
|
||||
// as a response with antiforgery token must never be cached.
|
||||
SetDoNotCacheHeaders(httpContext);
|
||||
|
||||
return tokenSet;
|
||||
}
|
||||
|
||||
|
|
@ -237,6 +242,10 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
{
|
||||
_logger.ReusedCookieToken();
|
||||
}
|
||||
|
||||
// Explicitly set the cache headers to 'no-cache'. This could override any user set value but this is fine
|
||||
// as a response with antiforgery token must never be cached.
|
||||
SetDoNotCacheHeaders(httpContext);
|
||||
}
|
||||
|
||||
private void SaveCookieTokenAndHeader(HttpContext httpContext, string cookieToken)
|
||||
|
|
@ -358,6 +367,43 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
return antiforgeryFeature;
|
||||
}
|
||||
|
||||
private void SetDoNotCacheHeaders(HttpContext httpContext)
|
||||
{
|
||||
// Since antifogery token generation is not very obvious to the end users (ex: MVC's form tag generates them
|
||||
// by default), log a warning to let users know of the change in behavior to any cache headers they might
|
||||
// have set explicitly.
|
||||
LogCacheHeaderOverrideWarning(httpContext.Response);
|
||||
|
||||
httpContext.Response.Headers[HeaderNames.CacheControl] = "no-cache";
|
||||
httpContext.Response.Headers[HeaderNames.Pragma] = "no-cache";
|
||||
}
|
||||
|
||||
private void LogCacheHeaderOverrideWarning(HttpResponse response)
|
||||
{
|
||||
var logWarning = false;
|
||||
CacheControlHeaderValue cacheControlHeaderValue;
|
||||
if (CacheControlHeaderValue.TryParse(response.Headers[HeaderNames.CacheControl], out cacheControlHeaderValue))
|
||||
{
|
||||
if (!cacheControlHeaderValue.NoCache)
|
||||
{
|
||||
logWarning = true;
|
||||
}
|
||||
}
|
||||
|
||||
var pragmaHeader = response.Headers[HeaderNames.Pragma];
|
||||
if (!logWarning
|
||||
&& !string.IsNullOrEmpty(pragmaHeader)
|
||||
&& string.Compare(pragmaHeader, "no-cache", ignoreCase: true) != 0)
|
||||
{
|
||||
logWarning = true;
|
||||
}
|
||||
|
||||
if (logWarning)
|
||||
{
|
||||
_logger.ResponseCacheHeadersOverridenToNoCache();
|
||||
}
|
||||
}
|
||||
|
||||
private AntiforgeryTokenSet Serialize(IAntiforgeryFeature antiforgeryFeature)
|
||||
{
|
||||
// Should only be called after new tokens have been generated.
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"dependencies": {
|
||||
"Microsoft.AspNetCore.DataProtection": "1.1.0-*",
|
||||
"Microsoft.AspNetCore.Http.Abstractions": "1.1.0-*",
|
||||
"Microsoft.AspNetCore.Http.Extensions": "1.1.0-*",
|
||||
"Microsoft.AspNetCore.WebUtilities": "1.1.0-*",
|
||||
"Microsoft.Extensions.ObjectPool": "1.1.0-*",
|
||||
"NETStandard.Library": "1.6.1-*"
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -15,6 +18,10 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
{
|
||||
public class DefaultAntiforgeryTest
|
||||
{
|
||||
private const string ResponseCacheHeadersOverrideWarningMessage =
|
||||
"The 'Cache-Control' and 'Pragma' headers have been overridden and set to 'no-cache' to prevent caching " +
|
||||
"of this response. Any response that uses antiforgery should not be cached.";
|
||||
|
||||
[Fact]
|
||||
public async Task ChecksSSL_ValidateRequestAsync_Throws()
|
||||
{
|
||||
|
|
@ -275,6 +282,67 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
Assert.Equal(context.TestTokenSet.FormTokenString, antiforgeryFeature.NewRequestTokenString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAndStoreTokens_ExistingValidCookieToken_NotOverriden_AndSetsDoNotCacheHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var antiforgeryFeature = new AntiforgeryFeature();
|
||||
var context = CreateMockContext(
|
||||
new AntiforgeryOptions(),
|
||||
useOldCookie: true,
|
||||
isOldCookieValid: true,
|
||||
antiforgeryFeature: antiforgeryFeature);
|
||||
var antiforgery = GetAntiforgery(context);
|
||||
|
||||
// Act
|
||||
var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);
|
||||
|
||||
// Assert
|
||||
// We shouldn't have saved the cookie because it already existed.
|
||||
context.TokenStore.Verify(
|
||||
t => t.SaveCookieToken(It.IsAny<HttpContext>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
|
||||
Assert.Null(tokenSet.CookieToken);
|
||||
Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken);
|
||||
|
||||
Assert.NotNull(antiforgeryFeature);
|
||||
Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers[HeaderNames.CacheControl]);
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers[HeaderNames.Pragma]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAndStoreTokens_ExistingCachingHeaders_Overriden()
|
||||
{
|
||||
// Arrange
|
||||
var antiforgeryFeature = new AntiforgeryFeature();
|
||||
var context = CreateMockContext(
|
||||
new AntiforgeryOptions(),
|
||||
useOldCookie: true,
|
||||
isOldCookieValid: true,
|
||||
antiforgeryFeature: antiforgeryFeature);
|
||||
var antiforgery = GetAntiforgery(context);
|
||||
context.HttpContext.Response.Headers["Cache-Control"] = "public";
|
||||
|
||||
// Act
|
||||
var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);
|
||||
|
||||
// Assert
|
||||
// We shouldn't have saved the cookie because it already existed.
|
||||
context.TokenStore.Verify(
|
||||
t => t.SaveCookieToken(It.IsAny<HttpContext>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
|
||||
Assert.Null(tokenSet.CookieToken);
|
||||
Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken);
|
||||
|
||||
Assert.NotNull(antiforgeryFeature);
|
||||
Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers[HeaderNames.CacheControl]);
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers[HeaderNames.Pragma]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAndStoreTokens_NoExistingCookieToken_Saved()
|
||||
{
|
||||
|
|
@ -309,6 +377,36 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
Assert.True(antiforgeryFeature.HaveStoredNewCookieToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAndStoreTokens_NoExistingCookieToken_Saved_AndSetsDoNotCacheHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var antiforgeryFeature = new AntiforgeryFeature();
|
||||
var context = CreateMockContext(
|
||||
new AntiforgeryOptions(),
|
||||
useOldCookie: false,
|
||||
isOldCookieValid: false,
|
||||
antiforgeryFeature: antiforgeryFeature);
|
||||
var antiforgery = GetAntiforgery(context);
|
||||
|
||||
// Act
|
||||
var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);
|
||||
|
||||
// Assert
|
||||
context.TokenStore.Verify(
|
||||
t => t.SaveCookieToken(It.IsAny<HttpContext>(), context.TestTokenSet.NewCookieTokenString),
|
||||
Times.Once);
|
||||
|
||||
Assert.Equal(context.TestTokenSet.NewCookieTokenString, tokenSet.CookieToken);
|
||||
Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken);
|
||||
|
||||
Assert.NotNull(antiforgeryFeature);
|
||||
Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
|
||||
Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers[HeaderNames.CacheControl]);
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers[HeaderNames.Pragma]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAndStoreTokens_DoesNotSerializeTwice()
|
||||
{
|
||||
|
|
@ -808,6 +906,76 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
Assert.Equal(expectedHeaderValue, xFrameOptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCookieTokenAndHeader_NewCookieToken_SetsDoNotCacheHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var options = new AntiforgeryOptions();
|
||||
var antiforgeryFeature = new AntiforgeryFeature();
|
||||
|
||||
// Generate a new cookie.
|
||||
var context = CreateMockContext(
|
||||
options,
|
||||
useOldCookie: false,
|
||||
isOldCookieValid: false,
|
||||
antiforgeryFeature: antiforgeryFeature);
|
||||
var antiforgery = GetAntiforgery(context);
|
||||
|
||||
// Act
|
||||
antiforgery.SetCookieTokenAndHeader(context.HttpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers["Cache-Control"]);
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCookieTokenAndHeader_ValidOldCookieToken_SetsDoNotCacheHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var options = new AntiforgeryOptions();
|
||||
var antiforgeryFeature = new AntiforgeryFeature();
|
||||
|
||||
// Generate a new cookie.
|
||||
var context = CreateMockContext(
|
||||
options,
|
||||
useOldCookie: true,
|
||||
isOldCookieValid: true,
|
||||
antiforgeryFeature: antiforgeryFeature);
|
||||
var antiforgery = GetAntiforgery(context);
|
||||
|
||||
// Act
|
||||
antiforgery.SetCookieTokenAndHeader(context.HttpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers["Cache-Control"]);
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCookieTokenAndHeader_OverridesExistingCachingHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var options = new AntiforgeryOptions();
|
||||
var antiforgeryFeature = new AntiforgeryFeature();
|
||||
|
||||
// Generate a new cookie.
|
||||
var context = CreateMockContext(
|
||||
options,
|
||||
useOldCookie: true,
|
||||
isOldCookieValid: true,
|
||||
antiforgeryFeature: antiforgeryFeature);
|
||||
var antiforgery = GetAntiforgery(context);
|
||||
context.HttpContext.Response.Headers["Cache-Control"] = "public";
|
||||
|
||||
// Act
|
||||
antiforgery.SetCookieTokenAndHeader(context.HttpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers["Cache-Control"]);
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, "SAMEORIGIN")]
|
||||
[InlineData(true, null)]
|
||||
|
|
@ -965,6 +1133,110 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
|
|||
context.TokenSerializer.Verify(s => s.Deserialize(null), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAndStoreTokens_DoesNotLogWarning_IfNoExistingCacheHeadersPresent()
|
||||
{
|
||||
// Arrange
|
||||
var testSink = new TestSink();
|
||||
var loggerFactory = new Mock<ILoggerFactory>();
|
||||
loggerFactory
|
||||
.Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName))
|
||||
.Returns(new TestLogger("test logger", testSink, enabled: true));
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(loggerFactory.Object);
|
||||
var antiforgeryFeature = new AntiforgeryFeature();
|
||||
var context = CreateMockContext(
|
||||
new AntiforgeryOptions(),
|
||||
useOldCookie: false,
|
||||
isOldCookieValid: false,
|
||||
antiforgeryFeature: antiforgeryFeature);
|
||||
context.HttpContext.RequestServices = services.BuildServiceProvider();
|
||||
var antiforgery = GetAntiforgery(context);
|
||||
|
||||
// Act
|
||||
var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);
|
||||
|
||||
// Assert
|
||||
var hasWarningMessage = testSink.Writes
|
||||
.Where(wc => wc.LogLevel == LogLevel.Warning)
|
||||
.Select(wc => wc.State?.ToString())
|
||||
.Contains(ResponseCacheHeadersOverrideWarningMessage);
|
||||
Assert.False(hasWarningMessage);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Cache-Control", "Public")]
|
||||
[InlineData("Cache-Control", "PuBlic")]
|
||||
[InlineData("Cache-Control", "Private")]
|
||||
[InlineData("Cache-Control", "PriVate")]
|
||||
[InlineData("Cache-Control", "No-Store")]
|
||||
[InlineData("Cache-Control", "No-store")]
|
||||
[InlineData("Pragma", "Foo")]
|
||||
public void GetAndStoreTokens_LogsWarning_NonNoCacheHeadersAlreadyPresent(string headerName, string headerValue)
|
||||
{
|
||||
// Arrange
|
||||
var testSink = new TestSink();
|
||||
var loggerFactory = new Mock<ILoggerFactory>();
|
||||
loggerFactory
|
||||
.Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName))
|
||||
.Returns(new TestLogger("test logger", testSink, enabled: true));
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(loggerFactory.Object);
|
||||
var antiforgeryFeature = new AntiforgeryFeature();
|
||||
var context = CreateMockContext(
|
||||
new AntiforgeryOptions(),
|
||||
useOldCookie: false,
|
||||
isOldCookieValid: false,
|
||||
antiforgeryFeature: antiforgeryFeature);
|
||||
context.HttpContext.RequestServices = services.BuildServiceProvider();
|
||||
var antiforgery = GetAntiforgery(context);
|
||||
context.HttpContext.Response.Headers[headerName] = headerValue;
|
||||
|
||||
// Act
|
||||
var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);
|
||||
|
||||
// Assert
|
||||
var hasWarningMessage = testSink.Writes
|
||||
.Where(wc => wc.LogLevel == LogLevel.Warning)
|
||||
.Select(wc => wc.State?.ToString())
|
||||
.Contains(ResponseCacheHeadersOverrideWarningMessage);
|
||||
Assert.True(hasWarningMessage);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Cache-Control", "no-cache")]
|
||||
[InlineData("Pragma", "no-cache")]
|
||||
public void GetAndStoreTokens_DoesNotLogsWarning_ForNoCacheHeaders_AlreadyPresent(string headerName, string headerValue)
|
||||
{
|
||||
// Arrange
|
||||
var testSink = new TestSink();
|
||||
var loggerFactory = new Mock<ILoggerFactory>();
|
||||
loggerFactory
|
||||
.Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName))
|
||||
.Returns(new TestLogger("test logger", testSink, enabled: true));
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(loggerFactory.Object);
|
||||
var antiforgeryFeature = new AntiforgeryFeature();
|
||||
var context = CreateMockContext(
|
||||
new AntiforgeryOptions(),
|
||||
useOldCookie: false,
|
||||
isOldCookieValid: false,
|
||||
antiforgeryFeature: antiforgeryFeature);
|
||||
context.HttpContext.RequestServices = services.BuildServiceProvider();
|
||||
var antiforgery = GetAntiforgery(context);
|
||||
context.HttpContext.Response.Headers[headerName] = headerValue;
|
||||
|
||||
// Act
|
||||
var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);
|
||||
|
||||
// Assert
|
||||
var hasWarningMessage = testSink.Writes
|
||||
.Where(wc => wc.LogLevel == LogLevel.Warning)
|
||||
.Select(wc => wc.State?.ToString())
|
||||
.Contains(ResponseCacheHeadersOverrideWarningMessage);
|
||||
Assert.False(hasWarningMessage);
|
||||
}
|
||||
|
||||
private DefaultAntiforgery GetAntiforgery(
|
||||
HttpContext httpContext,
|
||||
AntiforgeryOptions options = null,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"Microsoft.AspNetCore.Testing": "1.1.0-*",
|
||||
"Microsoft.Extensions.DependencyInjection": "1.1.0-*",
|
||||
"Microsoft.Extensions.Logging": "1.1.0-*",
|
||||
"Microsoft.Extensions.Logging.Testing": "1.1.0-*",
|
||||
"Microsoft.Extensions.WebEncoders": "1.1.0-*",
|
||||
"Moq": "4.6.36-*",
|
||||
"xunit": "2.2.0-*"
|
||||
|
|
|
|||
Loading…
Reference in New Issue