| File: Internal\DefaultAntiforgery.cs | Web Access |
| Project: src\aspnetcore\src\Antiforgery\src\Microsoft.AspNetCore.Antiforgery.csproj (Microsoft.AspNetCore.Antiforgery) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Antiforgery; /// <summary> /// Provides access to the antiforgery system, which provides protection against /// Cross-site Request Forgery (XSRF, also called CSRF) attacks. /// </summary> internal sealed class DefaultAntiforgery : IAntiforgery { private readonly AntiforgeryOptions _options; private readonly IAntiforgeryTokenGenerator _tokenGenerator; private readonly IAntiforgeryTokenSerializer _tokenSerializer; private readonly IAntiforgeryTokenStore _tokenStore; private readonly ILogger<DefaultAntiforgery> _logger; public DefaultAntiforgery( IOptions<AntiforgeryOptions> antiforgeryOptionsAccessor, IAntiforgeryTokenGenerator tokenGenerator, IAntiforgeryTokenSerializer tokenSerializer, IAntiforgeryTokenStore tokenStore, ILoggerFactory loggerFactory) { _options = antiforgeryOptionsAccessor.Value; _tokenGenerator = tokenGenerator; _tokenSerializer = tokenSerializer; _tokenStore = tokenStore; _logger = loggerFactory.CreateLogger<DefaultAntiforgery>(); } /// <inheritdoc /> public AntiforgeryTokenSet GetAndStoreTokens(HttpContext httpContext) { ArgumentNullException.ThrowIfNull(httpContext); CheckSSLConfig(httpContext); var antiforgeryFeature = GetTokensInternal(httpContext); var tokenSet = Serialize(antiforgeryFeature); if (!antiforgeryFeature.HaveStoredNewCookieToken) { if (antiforgeryFeature.NewCookieToken != null) { // Serialize handles the new cookie token string. Debug.Assert(antiforgeryFeature.NewCookieTokenString != null); SaveCookieTokenAndHeader(httpContext, antiforgeryFeature.NewCookieTokenString); antiforgeryFeature.HaveStoredNewCookieToken = true; _logger.NewCookieToken(); } else { _logger.ReusedCookieToken(); } } if (!httpContext.Response.HasStarted) { // 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; } /// <inheritdoc /> public AntiforgeryTokenSet GetTokens(HttpContext httpContext) { ArgumentNullException.ThrowIfNull(httpContext); CheckSSLConfig(httpContext); var antiforgeryFeature = GetTokensInternal(httpContext); return Serialize(antiforgeryFeature); } /// <inheritdoc /> public async Task<bool> IsRequestValidAsync(HttpContext httpContext) { ArgumentNullException.ThrowIfNull(httpContext); CheckSSLConfig(httpContext); var method = httpContext.Request.Method; if (SafeHttpMethods.IsSafe(method)) { return true; } var tokens = await _tokenStore.GetRequestTokensAsync(httpContext); if (tokens.CookieToken == null) { _logger.MissingCookieToken(_options.Cookie.Name); return false; } if (tokens.RequestToken == null) { _logger.MissingRequestToken(_options.FormFieldName, _options.HeaderName); return false; } // Extract cookie & request tokens if (!TryDeserializeTokens(httpContext, tokens, out var deserializedCookieToken, out var deserializedRequestToken)) { return false; } // Validate var result = _tokenGenerator.TryValidateTokenSet( httpContext, deserializedCookieToken, deserializedRequestToken, out var message); if (result) { _logger.ValidatedAntiforgeryToken(); } else { _logger.ValidationFailed(message!); } return result; } /// <inheritdoc /> public async Task ValidateRequestAsync(HttpContext httpContext) { ArgumentNullException.ThrowIfNull(httpContext); CheckSSLConfig(httpContext); var tokens = await _tokenStore.GetRequestTokensAsync(httpContext); if (tokens.CookieToken == null) { throw new AntiforgeryValidationException( Resources.FormatAntiforgery_CookieToken_MustBeProvided(_options.Cookie.Name)); } if (tokens.RequestToken == null) { if (_options.HeaderName == null) { var message = Resources.FormatAntiforgery_FormToken_MustBeProvided(_options.FormFieldName); throw new AntiforgeryValidationException(message); } else if (!httpContext.Request.HasFormContentType) { var message = Resources.FormatAntiforgery_HeaderToken_MustBeProvided(_options.HeaderName); throw new AntiforgeryValidationException(message); } else { var message = Resources.FormatAntiforgery_RequestToken_MustBeProvided( _options.FormFieldName, _options.HeaderName); throw new AntiforgeryValidationException(message); } } ValidateTokens(httpContext, tokens); _logger.ValidatedAntiforgeryToken(); } private void ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet) { Debug.Assert(!string.IsNullOrEmpty(antiforgeryTokenSet.CookieToken)); Debug.Assert(!string.IsNullOrEmpty(antiforgeryTokenSet.RequestToken)); // Extract cookie & request tokens AntiforgeryToken deserializedCookieToken; AntiforgeryToken deserializedRequestToken; DeserializeTokens( httpContext, antiforgeryTokenSet, out deserializedCookieToken, out deserializedRequestToken); // Validate if (!_tokenGenerator.TryValidateTokenSet( httpContext, deserializedCookieToken, deserializedRequestToken, out var message)) { throw new AntiforgeryValidationException(message); } } /// <inheritdoc /> public void SetCookieTokenAndHeader(HttpContext httpContext) { ArgumentNullException.ThrowIfNull(httpContext); CheckSSLConfig(httpContext); var antiforgeryFeature = GetCookieTokens(httpContext); if (!antiforgeryFeature.HaveStoredNewCookieToken && antiforgeryFeature.NewCookieToken != null) { if (antiforgeryFeature.NewCookieTokenString == null) { antiforgeryFeature.NewCookieTokenString = _tokenSerializer.Serialize(antiforgeryFeature.NewCookieToken); } SaveCookieTokenAndHeader(httpContext, antiforgeryFeature.NewCookieTokenString); antiforgeryFeature.HaveStoredNewCookieToken = true; _logger.NewCookieToken(); } else { _logger.ReusedCookieToken(); } if (!httpContext.Response.HasStarted) { SetDoNotCacheHeaders(httpContext); } } private void SaveCookieTokenAndHeader(HttpContext httpContext, string cookieToken) { if (cookieToken != null) { // Persist the new cookie if it is not null. _tokenStore.SaveCookieToken(httpContext, cookieToken); } if (!_options.SuppressXFrameOptionsHeader && !httpContext.Response.Headers.ContainsKey(HeaderNames.XFrameOptions)) { // Adding X-Frame-Options header to prevent ClickJacking. See // http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-10 // for more information. httpContext.Response.Headers.XFrameOptions = "SAMEORIGIN"; } } private void CheckSSLConfig(HttpContext context) { if (_options.Cookie.SecurePolicy == CookieSecurePolicy.Always && !context.Request.IsHttps) { throw new InvalidOperationException(Resources.FormatAntiforgery_RequiresSSL( string.Join(".", nameof(AntiforgeryOptions), nameof(AntiforgeryOptions.Cookie), nameof(CookieBuilder.SecurePolicy)), nameof(CookieSecurePolicy.Always))); } } private static IAntiforgeryFeature GetAntiforgeryFeature(HttpContext httpContext) { var antiforgeryFeature = httpContext.Features.Get<IAntiforgeryFeature>(); if (antiforgeryFeature is null) { antiforgeryFeature = new AntiforgeryFeature(); httpContext.Features.Set(antiforgeryFeature); } return antiforgeryFeature; } private IAntiforgeryFeature GetCookieTokens(HttpContext httpContext) { var antiforgeryFeature = GetAntiforgeryFeature(httpContext); if (antiforgeryFeature.HaveGeneratedNewCookieToken) { Debug.Assert(antiforgeryFeature.HaveDeserializedCookieToken); // Have executed this method earlier in the context of this request. return antiforgeryFeature; } AntiforgeryToken? cookieToken; if (antiforgeryFeature.HaveDeserializedCookieToken) { cookieToken = antiforgeryFeature.CookieToken; } else { cookieToken = GetCookieTokenDoesNotThrow(httpContext); antiforgeryFeature.CookieToken = cookieToken; antiforgeryFeature.HaveDeserializedCookieToken = true; } AntiforgeryToken? newCookieToken; if (_tokenGenerator.IsCookieTokenValid(cookieToken)) { // No need for the cookie token from the request after it has been verified. newCookieToken = null; } else { // Need to make sure we're always operating with a good cookie token. newCookieToken = _tokenGenerator.GenerateCookieToken(); Debug.Assert(_tokenGenerator.IsCookieTokenValid(newCookieToken)); } antiforgeryFeature.HaveGeneratedNewCookieToken = true; antiforgeryFeature.NewCookieToken = newCookieToken; return antiforgeryFeature; } private AntiforgeryToken? GetCookieTokenDoesNotThrow(HttpContext httpContext) { try { var serializedToken = _tokenStore.GetCookieToken(httpContext); if (serializedToken != null) { var token = _tokenSerializer.Deserialize(serializedToken); return token; } } catch (Exception ex) { // ignore failures since we'll just generate a new token _logger.TokenDeserializeException(ex); } return null; } private IAntiforgeryFeature GetTokensInternal(HttpContext httpContext) { var antiforgeryFeature = GetCookieTokens(httpContext); if (antiforgeryFeature.NewRequestToken == null) { var cookieToken = antiforgeryFeature.NewCookieToken ?? antiforgeryFeature.CookieToken; antiforgeryFeature.NewRequestToken = _tokenGenerator.GenerateRequestToken( httpContext, cookieToken!); } return antiforgeryFeature; } /// <summary> /// Sets the 'Cache-Control' header to 'no-cache, no-store' and 'Pragma' header to 'no-cache' overriding any user set value. /// </summary> /// <param name="httpContext">The <see cref="HttpContext"/>.</param> private void SetDoNotCacheHeaders(HttpContext httpContext) { var logWarning = false; var responseHeaders = httpContext.Response.Headers; if (responseHeaders.TryGetValue(HeaderNames.CacheControl, out var cacheControlHeader) && CacheControlHeaderValue.TryParse(cacheControlHeader.ToString(), out var cacheControlHeaderValue)) { // If the Cache-Control is already set, override it only if required if (!cacheControlHeaderValue.NoCache || !cacheControlHeaderValue.NoStore) { logWarning = true; responseHeaders.CacheControl = "no-cache, no-store"; } } else { responseHeaders.CacheControl = "no-cache, no-store"; } if (responseHeaders.TryGetValue(HeaderNames.Pragma, out var pragmaHeader) && pragmaHeader.Count > 0) { // If the Pragma is already set, override it only if required if (!string.Equals(pragmaHeader[0], "no-cache", StringComparison.OrdinalIgnoreCase)) { logWarning = true; httpContext.Response.Headers.Pragma = "no-cache"; } } else { httpContext.Response.Headers.Pragma = "no-cache"; } // Since antiforgery 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. if (logWarning) { _logger.ResponseCacheHeadersOverridenToNoCache(); } } private AntiforgeryTokenSet Serialize(IAntiforgeryFeature antiforgeryFeature) { // Should only be called after new tokens have been generated. Debug.Assert(antiforgeryFeature.HaveGeneratedNewCookieToken); Debug.Assert(antiforgeryFeature.NewRequestToken != null); if (antiforgeryFeature.NewRequestTokenString == null) { antiforgeryFeature.NewRequestTokenString = _tokenSerializer.Serialize(antiforgeryFeature.NewRequestToken); } if (antiforgeryFeature.NewCookieTokenString == null && antiforgeryFeature.NewCookieToken != null) { antiforgeryFeature.NewCookieTokenString = _tokenSerializer.Serialize(antiforgeryFeature.NewCookieToken); } return new AntiforgeryTokenSet( antiforgeryFeature.NewRequestTokenString, antiforgeryFeature.NewCookieTokenString!, _options.FormFieldName, _options.HeaderName); } private bool TryDeserializeTokens( HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet, [NotNullWhen(true)] out AntiforgeryToken? cookieToken, [NotNullWhen(true)] out AntiforgeryToken? requestToken) { try { DeserializeTokens(httpContext, antiforgeryTokenSet, out cookieToken, out requestToken); return true; } catch (AntiforgeryValidationException ex) { _logger.FailedToDeserialzeTokens(ex); cookieToken = null; requestToken = null; return false; } } private void DeserializeTokens( HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet, out AntiforgeryToken cookieToken, out AntiforgeryToken requestToken) { var antiforgeryFeature = GetAntiforgeryFeature(httpContext); if (antiforgeryFeature.HaveDeserializedCookieToken) { cookieToken = antiforgeryFeature.CookieToken!; } else { cookieToken = _tokenSerializer.Deserialize(antiforgeryTokenSet.CookieToken!); antiforgeryFeature.CookieToken = cookieToken; antiforgeryFeature.HaveDeserializedCookieToken = true; } if (antiforgeryFeature.HaveDeserializedRequestToken) { requestToken = antiforgeryFeature.RequestToken!; } else { requestToken = _tokenSerializer.Deserialize(antiforgeryTokenSet.RequestToken!); antiforgeryFeature.RequestToken = requestToken; antiforgeryFeature.HaveDeserializedRequestToken = true; } } }