diff --git a/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs b/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs index 242fd47857..06b6472506 100644 --- a/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs +++ b/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -11,20 +12,20 @@ namespace Umbraco.Cms.Api.Delivery.Handlers; internal sealed class InitializeMemberApplicationNotificationHandler : INotificationAsyncHandler { - private readonly IMemberApplicationManager _memberApplicationManager; private readonly IRuntimeState _runtimeState; private readonly ILogger _logger; private readonly DeliveryApiSettings _deliveryApiSettings; + private readonly IServiceScopeFactory _serviceScopeFactory; public InitializeMemberApplicationNotificationHandler( - IMemberApplicationManager memberApplicationManager, IRuntimeState runtimeState, IOptions deliveryApiSettings, - ILogger logger) + ILogger logger, + IServiceScopeFactory serviceScopeFactory) { - _memberApplicationManager = memberApplicationManager; _runtimeState = runtimeState; _logger = logger; + _serviceScopeFactory = serviceScopeFactory; _deliveryApiSettings = deliveryApiSettings.Value; } @@ -35,26 +36,31 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica return; } + // we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during + // install that is not allowed (no connection string means no DbContext) + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService(); + if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true) { - await _memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken); + await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken); return; } if (ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls) is false) { - await _memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken); + await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken); return; } if (_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls.Any() && ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls) is false) { - await _memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken); + await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken); return; } - await _memberApplicationManager.EnsureMemberApplicationAsync( + await memberApplicationManager.EnsureMemberApplicationAsync( _deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls, _deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls, cancellationToken); diff --git a/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs b/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs index 3d7e01a0ad..7694f83dd2 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs @@ -1,14 +1,10 @@ -using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Serilog; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DistributedLocking; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Persistence.EFCore.Factories; using Umbraco.Cms.Persistence.EFCore.Locking; using Umbraco.Cms.Persistence.EFCore.Scoping; @@ -22,13 +18,6 @@ public static class UmbracoEFCoreServiceCollectionExtensions public static IServiceCollection AddUmbracoEFCoreContext(this IServiceCollection services, DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction = null) where T : DbContext { - var optionsBuilder = new DbContextOptionsBuilder(); - services.TryAddSingleton>( - sp => - { - SetupDbContext(defaultEFCoreOptionsAction, sp, optionsBuilder); - return new UmbracoPooledDbContextFactory(sp.GetRequiredService(), optionsBuilder.Options); - }); services.AddPooledDbContextFactory((provider, builder) => SetupDbContext(defaultEFCoreOptionsAction, provider, builder)); services.AddTransient(services => services.GetRequiredService>().CreateDbContext()); @@ -52,13 +41,6 @@ public static class UmbracoEFCoreServiceCollectionExtensions connectionString = connectionString.Replace(Constants.System.DataDirectoryPlaceholder, dataDirectory); } - var optionsBuilder = new DbContextOptionsBuilder(); - services.TryAddSingleton>( - sp => - { - defaultEFCoreOptionsAction?.Invoke(optionsBuilder, providerName, connectionString); - return new UmbracoPooledDbContextFactory(sp.GetRequiredService(), optionsBuilder.Options); - }); services.AddPooledDbContextFactory(options => defaultEFCoreOptionsAction?.Invoke(options, providerName, connectionString)); services.AddTransient(services => services.GetRequiredService>().CreateDbContext()); @@ -99,13 +81,6 @@ public static class UmbracoEFCoreServiceCollectionExtensions { optionsAction ??= (sp, options) => { }; - var optionsBuilder = new DbContextOptionsBuilder(); - - services.TryAddSingleton>(sp => - { - optionsAction.Invoke(sp, optionsBuilder); - return new UmbracoPooledDbContextFactory(sp.GetRequiredService(), optionsBuilder.Options); - }); services.AddPooledDbContextFactory(optionsAction); services.AddTransient(services => services.GetRequiredService>().CreateDbContext()); diff --git a/src/Umbraco.Cms.Persistence.EFCore/Factories/UmbracoPooledDbContextFactory.cs b/src/Umbraco.Cms.Persistence.EFCore/Factories/UmbracoPooledDbContextFactory.cs deleted file mode 100644 index de0f7db200..0000000000 --- a/src/Umbraco.Cms.Persistence.EFCore/Factories/UmbracoPooledDbContextFactory.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Services; - -namespace Umbraco.Cms.Persistence.EFCore.Factories; - -/// -internal class UmbracoPooledDbContextFactory : PooledDbContextFactory - where TContext : DbContext -{ - private readonly IRuntimeState _runtimeState; - private readonly DbContextOptions _options; - - /// - public UmbracoPooledDbContextFactory(IRuntimeState runtimeState, DbContextOptions options, int poolSize = 1024 /*DbContextPool.DefaultPoolSize*/) : base(options, poolSize) - { - _runtimeState = runtimeState; - _options = options; - } - - /// - public override TContext CreateDbContext() - { - if (_runtimeState.Level == RuntimeLevel.Run) - { - return base.CreateDbContext(); - } - else - { - return (TContext?)Activator.CreateInstance(typeof(TContext), _options) ?? throw new InvalidOperationException("Unable to create DbContext"); - } - } - - /// - public override async Task CreateDbContextAsync(CancellationToken cancellationToken = default) - { - if (_runtimeState.Level == RuntimeLevel.Run) - { - return await base.CreateDbContextAsync(cancellationToken); - } - else - { - return (TContext?)Activator.CreateInstance(typeof(TContext), _options) ?? throw new InvalidOperationException("Unable to create DbContext"); - } - } -} diff --git a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs index a27630bd34..3df757ee15 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs @@ -1,3 +1,4 @@ +using System.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.DependencyInjection; @@ -55,6 +56,10 @@ public class UmbracoDbContext : DbContext { ILogger logger = StaticServiceProvider.Instance.GetRequiredService>(); logger.LogCritical("No connection string was found, cannot setup Umbraco EF Core context"); + + // we're throwing an exception here to make it abundantly clear that one should never utilize (or have a + // dependency on) the DbContext before the connection string has been initialized by the installer. + throw new ConfigurationErrorsException("No connection string was found, cannot setup Umbraco EF Core context"); } IEnumerable migrationProviders = StaticServiceProvider.Instance.GetServices(); diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs index 5394cdc275..2a89327a33 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs @@ -66,7 +66,17 @@ public class ExamineIndexRebuilder : IIndexRebuilder _logger.LogInformation("Starting async background thread for rebuilding index {indexName}.", indexName); _backgroundTaskQueue.QueueBackgroundWorkItem( - cancellationToken => Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken))); + cancellationToken => + { + // Do not flow AsyncLocal to the child thread + using (ExecutionContext.SuppressFlow()) + { + Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken)); + + // immediately return so the queue isn't waiting. + return Task.CompletedTask; + } + }); } else { @@ -96,12 +106,16 @@ public class ExamineIndexRebuilder : IIndexRebuilder _backgroundTaskQueue.QueueBackgroundWorkItem( cancellationToken => { - // This is a fire/forget task spawned by the background thread queue (which means we - // don't need to worry about ExecutionContext flowing). - Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken)); + // Do not flow AsyncLocal to the child thread + using (ExecutionContext.SuppressFlow()) + { + // This is a fire/forget task spawned by the background thread queue (which means we + // don't need to worry about ExecutionContext flowing). + Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken)); - // immediately return so the queue isn't waiting. - return Task.CompletedTask; + // immediately return so the queue isn't waiting. + return Task.CompletedTask; + } }); } else diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 61f0fe126d..8152eb70e9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -311,7 +311,12 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository definition.Name.Replace(' ', '_'))); Directory.CreateDirectory(directoryName); + var expectedRoot = _hostingEnvironment.MapPathContentRoot(_createdPackagesFolderPath); var finalPackagePath = Path.Combine(directoryName, fileName); + if (finalPackagePath.StartsWith(expectedRoot) == false) + { + throw new IOException("Invalid path due to the package name"); + } // Clean existing files foreach (var packagePath in new[] { definition.PackagePath, finalPackagePath }) diff --git a/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs b/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs index 1820f2b5e0..b9980308b9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs @@ -1,9 +1,12 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Web.BackOffice.Controllers; +[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] public class AnalyticsController : UmbracoAuthorizedJsonController { private readonly IMetricsConsentService _metricsConsentService; diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index beb8787c79..19cc4ae70f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -404,6 +404,9 @@ public class AuthenticationController : UmbracoApiControllerBase } BackOfficeIdentityUser? identityUser = await _userManager.FindByEmailAsync(model.Email); + + await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500)); // To randomize response time preventing user enumeration + if (identityUser != null) { IUser? user = _userService.GetByEmail(model.Email); @@ -424,14 +427,20 @@ public class AuthenticationController : UmbracoApiControllerBase var mailMessage = new EmailMessage(from, user.Email, subject, message, true); - await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.PasswordReset, true); + try + { + await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.PasswordReset, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending email, please check your SMTP configuration: {ErrorMessage}", ex.Message); + return Ok(); + } _userManager.NotifyForgotPasswordRequested(User, user.Id.ToString()); } } - await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500)); - return Ok(); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 4cb7dc52fc..cef2352ab4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -18,6 +18,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers; /// Backoffice controller supporting the dashboard for language administration. /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] +[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] public class LanguageController : UmbracoAuthorizedJsonController { private readonly ILocalizationService _localizationService; diff --git a/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs index c8c391d990..91cd16a0f6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs @@ -1,13 +1,16 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers; [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] +[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] public class PublishedSnapshotCacheStatusController : UmbracoAuthorizedApiController { private readonly DistributedCache _distributedCache; diff --git a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs index d87398d574..34cd759dbb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using System.Security; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -14,11 +15,13 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers; [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] +[Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] public class RedirectUrlManagementController : UmbracoAuthorizedApiController { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; @@ -45,6 +48,8 @@ public class RedirectUrlManagementController : UmbracoAuthorizedApiController _configManipulator = configManipulator ?? throw new ArgumentNullException(nameof(configManipulator)); } + private bool IsEnabled => _webRoutingSettings.CurrentValue.DisableRedirectUrlTracking == false; + /// /// Returns true/false of whether redirect tracking is enabled or not /// @@ -52,9 +57,8 @@ public class RedirectUrlManagementController : UmbracoAuthorizedApiController [HttpGet] public IActionResult GetEnableState() { - var enabled = _webRoutingSettings.CurrentValue.DisableRedirectUrlTracking == false; var userIsAdmin = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.IsAdmin() ?? false; - return Ok(new { enabled, userIsAdmin }); + return Ok(new { enabled = IsEnabled, userIsAdmin }); } //add paging @@ -104,6 +108,11 @@ public class RedirectUrlManagementController : UmbracoAuthorizedApiController [HttpPost] public IActionResult DeleteRedirectUrl(Guid id) { + if (IsEnabled is false) + { + return BadRequest("Redirect URL tracking is disabled, and therefore no URLs can be deleted."); + } + _redirectUrlService.Delete(id); return Ok(); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs b/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs index 32adfcfdf6..616edfa04f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs @@ -1,8 +1,10 @@ +using Microsoft.AspNetCore.Authorization; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; using Stylesheet = Umbraco.Cms.Core.Models.ContentEditing.Stylesheet; @@ -12,6 +14,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers; /// The API controller used for retrieving available stylesheets /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] +[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] public class StylesheetController : UmbracoAuthorizedJsonController { private readonly IFileService _fileService; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index e09718176c..7b0a10cf31 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -28,7 +28,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { * }); * * @returns {Promise} resourcePromise object - * + * */ get2FAProviders: function () { @@ -203,7 +203,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { "PostRequestPasswordReset"), { email: email }), - 'Request password reset failed for email ' + email); + 'An email with password reset instructions will be sent to the specified address if it matched our records'); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js index 13ca4cb193..ec039dfdd7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js @@ -11,11 +11,12 @@ angular.module('umbraco').controller("Umbraco.LoginController", function (events //check if there's a returnPath query string, if so redirect to it var locationObj = $location.search(); if (locationObj.returnPath) { - // decodeURIComponent(...) does not play nice with OAuth redirect URLs, so until we have a - // dedicated login screen for the new back-office, we need to hardcode this exception - path = locationObj.returnPath.indexOf("/security/back-office/authorize") > 0 - ? locationObj.returnPath - : decodeURIComponent(locationObj.returnPath); + // ensure that the returnPath is a valid URL under the current origin (prevents DOM-XSS among other things) + const returnPath = decodeURIComponent(locationObj.returnPath); + const url = new URL(returnPath, window.location.origin); + if (url.origin === window.location.origin) { + path = returnPath; + } } // Ensure path is not absolute diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index 4a9dc85865..69dc038cb8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -158,7 +158,7 @@
- +