diff --git a/backend/src/Squidex.Data.MongoDb/MongoClientFactory.cs b/backend/src/Squidex.Data.MongoDb/MongoClientFactory.cs index 32089269d..2ae3301c4 100644 --- a/backend/src/Squidex.Data.MongoDb/MongoClientFactory.cs +++ b/backend/src/Squidex.Data.MongoDb/MongoClientFactory.cs @@ -8,6 +8,7 @@ using System.Text.Json; using MongoDB.Bson; using MongoDB.Driver; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; @@ -27,9 +28,12 @@ public static class MongoClientFactory // Register the serializers first. BsonDomainIdSerializer.Register(); BsonEscapedDictionarySerializer.Register(); + BsonEscapedDictionarySerializer.Register(); + BsonEscapedDictionarySerializer.Register(); BsonInstantSerializer.Register(); BsonJsonValueSerializer.Register(); BsonStringSerializer.Register(); + BsonStringSerializer.Register(); BsonUniqueContentIdSerializer.Register(); BsonJsonConvention.Register(jsonSerializerOptions, representation); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupOptions.cs new file mode 100644 index 000000000..5e8ebe1f1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupOptions.cs @@ -0,0 +1,13 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class BackupOptions +{ + public bool AllowRestoreFromLocalFiles { get; set; } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs index 6eb3f88cb..9db716232 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs @@ -6,13 +6,19 @@ // ========================================================================== using System.Diagnostics.CodeAnalysis; +using System.Security; +using Microsoft.Extensions.Options; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Entities.Backup; [ExcludeFromCodeCoverage] -public sealed class TempFolderBackupArchiveLocation(IJsonSerializer serializer, IHttpClientFactory httpClientFactory) : IBackupArchiveLocation +public sealed class TempFolderBackupArchiveLocation( + IJsonSerializer serializer, + IOptions options, + IHttpClientFactory httpClientFactory) + : IBackupArchiveLocation { public async Task OpenReaderAsync(Uri url, DomainId id, CancellationToken ct) @@ -21,6 +27,11 @@ public sealed class TempFolderBackupArchiveLocation(IJsonSerializer serializer, if (string.Equals(url.Scheme, "file", StringComparison.OrdinalIgnoreCase)) { + if (!options.Value.AllowRestoreFromLocalFiles) + { + throw new SecurityException("Downloading backups from local files not allowed. Permit them with BACKUPS__ALLOWRESTOREFROMLOCALFILES=true"); + } + stream = new FileStream(url.LocalPath, FileMode.Open, FileAccess.Read); } else diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs index 2417afeb3..0c1303207 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs @@ -73,8 +73,10 @@ public class EventConsumerProcessor : IEventSubscriber } } - // Acquire the lock to ensure any in-flight UpdateAsync has fully completed before returning. - using var _ = await asyncLock.EnterAsync(); + using (await asyncLock.EnterAsync()) + { + // Acquire the lock to ensure any in-flight UpdateAsync has fully completed before returning. + } } public virtual ValueTask OnNextAsync(IEventSubscription subscription, ParsedEvents @event) diff --git a/backend/src/Squidex/Areas/Api/Config/AssetFileResolver.cs b/backend/src/Squidex/Areas/Api/Config/AssetFileResolver.cs index a26df01cf..a7f15bdd5 100644 --- a/backend/src/Squidex/Areas/Api/Config/AssetFileResolver.cs +++ b/backend/src/Squidex/Areas/Api/Config/AssetFileResolver.cs @@ -85,7 +85,7 @@ public class AssetFileResolver(IAssetUsageTracker assetUsage, IUsageGate usageGa try { - using var httpClient = httpClientFactory.CreateClient(); + using var httpClient = httpClientFactory.CreateClient("Assets"); using var httpResponse = await httpClient.GetAsync(fileUrl, ct); var length = httpResponse.Content.Headers.ContentLength; diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index d5ebca676..ff47df2da 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Entities.Assets.Queries.Steps; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Search; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Http; namespace Squidex.Config.Domain; @@ -34,6 +35,9 @@ public static class AssetServices .As(); } + services.AddHttpClient("Assets") + .EnableSsrfProtection(); + services.AddSingletonAs() .AsSelf(); diff --git a/backend/src/Squidex/Config/Domain/BackupsServices.cs b/backend/src/Squidex/Config/Domain/BackupsServices.cs index c1c39448b..d052d3a10 100644 --- a/backend/src/Squidex/Config/Domain/BackupsServices.cs +++ b/backend/src/Squidex/Config/Domain/BackupsServices.cs @@ -12,15 +12,16 @@ using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Flows; -using Squidex.Infrastructure.Reflection; namespace Squidex.Config.Domain; public static class BackupsServices { - public static void AddSquidexBackups(this IServiceCollection services) + public static void AddSquidexBackups(this IServiceCollection services, IConfiguration config) { + services.Configure(config, + "backups"); + services.AddHttpClient("Backup", options => { options.Timeout = TimeSpan.FromHours(1); diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index 5475e56af..90b0a4c0d 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -21,6 +21,7 @@ using Squidex.Domain.Apps.Entities.Contents.Counter; using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.Diagnostics; +using Squidex.Infrastructure.Http; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.UsageTracking; @@ -45,7 +46,8 @@ public static class InfrastructureServices services.Configure(config, "diagnostics"); - services.AddHttpClient("Jint"); + services.AddHttpClient("Jint") + .EnableSsrfProtection(); services.AddReplicatedCache(); services.AddAsyncLocalCache(); diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index d19ebd7bb..f003ba996 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -37,7 +37,7 @@ public sealed class Startup(IConfiguration config) services.AddSquidexApps(config); services.AddSquidexAssetInfrastructure(config); services.AddSquidexAssets(config); - services.AddSquidexBackups(); + services.AddSquidexBackups(config); services.AddSquidexCollaborations(config); services.AddSquidexCommands(config); services.AddSquidexContents(config); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index a089b8f37..7400e8b64 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -101,6 +101,11 @@ "allowAutoRedirect": false }, + "backups": { + // Enables to download backups from the local file system. + "allowRestoreFromLocalFiles": false + }, + "caching": { // Set to true, to use strong etags. "strongETag": false,