diff --git a/OrchardCore.sln b/OrchardCore.sln index 524346a60db..1d0d2c672ea 100644 --- a/OrchardCore.sln +++ b/OrchardCore.sln @@ -432,6 +432,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OrchardCore.Themes", "Orcha src\OrchardCore.Themes\Directory.Build.targets = src\OrchardCore.Themes\Directory.Build.targets EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Security", "src\OrchardCore.Modules\OrchardCore.Security\OrchardCore.Security.csproj", "{B02C00A7-33C2-4FEE-9D0F-B14C349ADB68}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Abstractions.Tests", "test\OrchardCore.Abstractions.Tests\OrchardCore.Abstractions.Tests.csproj", "{FE8011DE-D917-4F74-9955-238B2BBA9165}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OrchardCore.Tests.Themes", "OrchardCore.Tests.Themes", "{61436BAE-FB36-4ADA-8E5D-EE64C4E04522}" @@ -1172,6 +1174,10 @@ Global {D00CF459-396D-49C9-92E2-3FD3C2A59847}.Debug|Any CPU.Build.0 = Debug|Any CPU {D00CF459-396D-49C9-92E2-3FD3C2A59847}.Release|Any CPU.ActiveCfg = Release|Any CPU {D00CF459-396D-49C9-92E2-3FD3C2A59847}.Release|Any CPU.Build.0 = Release|Any CPU + {B02C00A7-33C2-4FEE-9D0F-B14C349ADB68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B02C00A7-33C2-4FEE-9D0F-B14C349ADB68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B02C00A7-33C2-4FEE-9D0F-B14C349ADB68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B02C00A7-33C2-4FEE-9D0F-B14C349ADB68}.Release|Any CPU.Build.0 = Release|Any CPU {FE8011DE-D917-4F74-9955-238B2BBA9165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FE8011DE-D917-4F74-9955-238B2BBA9165}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE8011DE-D917-4F74-9955-238B2BBA9165}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1416,6 +1422,7 @@ Global {2BC850C3-9846-47E1-9068-AC0A8E5537AC} = {184139CF-C4AB-4FBE-AE19-54C8B3FE5C5E} {85EF279B-8F35-476D-9BBD-F503F20712B5} = {184139CF-C4AB-4FBE-AE19-54C8B3FE5C5E} {21F459C1-494E-41C9-B221-6C102774A47F} = {184139CF-C4AB-4FBE-AE19-54C8B3FE5C5E} + {B02C00A7-33C2-4FEE-9D0F-B14C349ADB68} = {A066395F-6F73-45DC-B5A6-B4E306110DCE} {FE8011DE-D917-4F74-9955-238B2BBA9165} = {B8D16C60-99B4-43D5-A3AD-4CD89AF39B25} {61436BAE-FB36-4ADA-8E5D-EE64C4E04522} = {B8D16C60-99B4-43D5-A3AD-4CD89AF39B25} {A493E5AD-9046-47F3-87A0-0D3AC7EF8699} = {B8D16C60-99B4-43D5-A3AD-4CD89AF39B25} diff --git a/mkdocs.yml b/mkdocs.yml index b0f38e0ae63..b43c5ac9876 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -212,6 +212,7 @@ nav: - Roles: docs/reference/modules/Roles/README.md - Sanitizer: docs/reference/core/Sanitizer/README.md - Scripting: docs/reference/modules/Scripting/README.md + - Security: docs/reference/modules/Security/README.md - Setup: docs/reference/modules/Setup/README.md - Shells: docs/reference/core/Shells/README.md - Tenants: docs/reference/modules/Tenants/README.md diff --git a/src/OrchardCore.Cms.Web/appsettings.json b/src/OrchardCore.Cms.Web/appsettings.json index 02a7c1988b0..3b8ae25aee9 100644 --- a/src/OrchardCore.Cms.Web/appsettings.json +++ b/src/OrchardCore.Cms.Web/appsettings.json @@ -55,6 +55,12 @@ // "Configuration": "192.168.99.100:6379,allowAdmin=true", // Redis Configuration string. // "InstancePrefix": "" // Optional prefix allowing a Redis instance to be shared by different applications. //}, + // See https://docs.orchardcore.net/en/latest/docs/reference/modules/Security/#security-settings-configuration to configure security settings. + //"OrchardCore_Security": { + // "ContentSecurityPolicy": {}, + // "PermissionsPolicy": { "fullscreen": "self" }, + // "ReferrerPolicy": "no-referrer" + //}, // See https://docs.orchardcore.net/en/latest/docs/reference/core/Shells/#enable-azure-shells-configuration to configure shell and tenant configuration in Azure Blob Storage. // Add a reference to the OrchardCore.Shells.Azure NuGet package. // Add '.AddAzureShellsConfiguration()' to your Host Startup AddOrchardCms() section. diff --git a/src/OrchardCore.Modules/OrchardCore.Security/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Security/AdminMenu.cs new file mode 100644 index 00000000000..9f29e1cef98 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/AdminMenu.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; +using OrchardCore.Navigation; +using OrchardCore.Security.Drivers; + +namespace OrchardCore.Security +{ + public class AdminMenu : INavigationProvider + { + private readonly IStringLocalizer S; + + public AdminMenu(IStringLocalizer localizer) + { + S = localizer; + } + + public Task BuildNavigationAsync(string name, NavigationBuilder builder) + { + if (String.Equals(name, "admin", StringComparison.OrdinalIgnoreCase)) + { + builder.Add(S["Security"], NavigationConstants.AdminMenuSecurityPosition, security => security + .AddClass("security").Id("security") + .Add(S["Settings"], settings => settings + .Add(S["Security Headers"], S["Security Headers"].PrefixPosition(), headers => headers + .Permission(SecurityPermissions.ManageSecurityHeadersSettings) + .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = SecuritySettingsDisplayDriver.SettingsGroupId }) + .LocalNav()))); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Drivers/SecuritySettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Security/Drivers/SecuritySettingsDisplayDriver.cs new file mode 100644 index 00000000000..1f6d6031c91 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Drivers/SecuritySettingsDisplayDriver.cs @@ -0,0 +1,123 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Environment.Shell; +using OrchardCore.Security.Options; +using OrchardCore.Security.Settings; +using OrchardCore.Security.ViewModels; +using OrchardCore.Settings; + +namespace OrchardCore.Security.Drivers +{ + public class SecuritySettingsDisplayDriver : SectionDisplayDriver + { + internal const string SettingsGroupId = "SecurityHeaders"; + + private readonly IShellHost _shellHost; + private readonly ShellSettings _shellSettings; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + private readonly SecuritySettings _securitySettings; + + public SecuritySettingsDisplayDriver( + IShellHost shellHost, + ShellSettings shellSettings, + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService, + IOptionsSnapshot securitySettings) + { + _shellHost = shellHost; + _shellSettings = shellSettings; + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + _securitySettings = securitySettings.Value; + } + + public override async Task EditAsync(SecuritySettings settings, BuildEditorContext context) + { + var user = _httpContextAccessor.HttpContext?.User; + + if (!await _authorizationService.AuthorizeAsync(user, SecurityPermissions.ManageSecurityHeadersSettings)) + { + return null; + } + + return Initialize("SecurityHeadersSettings_Edit", model => + { + // Set the settings from configuration when AdminSettings are overriden via ConfigureSecuritySettings() + var currentSettings = settings; + if (_securitySettings.FromConfiguration) + { + currentSettings = _securitySettings; + } + + model.FromConfiguration = currentSettings.FromConfiguration; + model.ContentSecurityPolicy = currentSettings.ContentSecurityPolicy; + model.PermissionsPolicy = currentSettings.PermissionsPolicy; + model.ReferrerPolicy = currentSettings.ReferrerPolicy; + + model.EnableSandbox = currentSettings.ContentSecurityPolicy != null && + currentSettings.ContentSecurityPolicy.ContainsKey(ContentSecurityPolicyValue.Sandbox); + + model.UpgradeInsecureRequests = currentSettings.ContentSecurityPolicy != null && + currentSettings.ContentSecurityPolicy.ContainsKey(ContentSecurityPolicyValue.UpgradeInsecureRequests); + }).Location("Content:2").OnGroup(SettingsGroupId); + } + + public override async Task UpdateAsync(SecuritySettings section, BuildEditorContext context) + { + var user = _httpContextAccessor.HttpContext?.User; + + if (!await _authorizationService.AuthorizeAsync(user, SecurityPermissions.ManageSecurityHeadersSettings)) + { + return null; + } + + if (context.GroupId == SettingsGroupId) + { + var model = new SecuritySettingsViewModel(); + + await context.Updater.TryUpdateModelAsync(model, Prefix); + + PrepareContentSecurityPolicyValues(model); + + section.ContentTypeOptions = SecurityHeaderDefaults.ContentTypeOptions; + section.ContentSecurityPolicy = model.ContentSecurityPolicy; + section.PermissionsPolicy = model.PermissionsPolicy; + section.ReferrerPolicy = model.ReferrerPolicy; + + if (context.Updater.ModelState.IsValid) + { + await _shellHost.ReleaseShellContextAsync(_shellSettings); + } + } + + return await EditAsync(section, context); + } + + private static void PrepareContentSecurityPolicyValues(SecuritySettingsViewModel model) + { + if (!model.EnableSandbox) + { + model.ContentSecurityPolicy.Remove(ContentSecurityPolicyValue.Sandbox); + } + else if (!model.ContentSecurityPolicy.TryGetValue(ContentSecurityPolicyValue.Sandbox, out _)) + { + model.ContentSecurityPolicy[ContentSecurityPolicyValue.Sandbox] = null; + } + + if (!model.UpgradeInsecureRequests) + { + model.ContentSecurityPolicy.Remove(ContentSecurityPolicyValue.UpgradeInsecureRequests); + } + else + { + model.ContentSecurityPolicy[ContentSecurityPolicyValue.UpgradeInsecureRequests] = null; + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Extensions/OrchardCoreBuilderExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Security/Extensions/OrchardCoreBuilderExtensions.cs new file mode 100644 index 00000000000..445ffc4fbb3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Extensions/OrchardCoreBuilderExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Configuration; +using OrchardCore.Environment.Shell.Configuration; +using OrchardCore.Security.Settings; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class OrchardCoreBuilderExtensions + { + public static OrchardCoreBuilder ConfigureSecuritySettings(this OrchardCoreBuilder builder) + { + builder.ConfigureServices((tenantServices, serviceProvider) => + { + var configurationSection = serviceProvider.GetRequiredService().GetSection("OrchardCore_Security"); + + tenantServices.PostConfigure(settings => + { + settings.ContentSecurityPolicy.Clear(); + settings.PermissionsPolicy.Clear(); + + configurationSection.Bind(settings); + + settings.FromConfiguration = true; + }); + }); + + return builder; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Extensions/SecurityHeadersApplicationBuilderExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Security/Extensions/SecurityHeadersApplicationBuilderExtensions.cs new file mode 100644 index 00000000000..9ab658bdce4 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Extensions/SecurityHeadersApplicationBuilderExtensions.cs @@ -0,0 +1,38 @@ +using System; +using OrchardCore.Security.Services; +using OrchardCore.Security.Options; + +namespace Microsoft.AspNetCore.Builder +{ + public static class SecurityHeadersApplicationBuilderExtensions + { + public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app, nameof(app)); + + return app.UseSecurityHeaders(new SecurityHeadersOptions()); + } + + public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder app, SecurityHeadersOptions options) + { + ArgumentNullException.ThrowIfNull(app, nameof(app)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); + + app.UseMiddleware(options); + + return app; + } + + public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder app, Action optionsAction) + { + ArgumentNullException.ThrowIfNull(app, nameof(app)); + ArgumentNullException.ThrowIfNull(optionsAction, nameof(optionsAction)); + + var options = new SecurityHeadersOptions(); + + optionsAction.Invoke(options); + + return app.UseSecurityHeaders(options); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Security/Manifest.cs new file mode 100644 index 00000000000..12a8230d4d2 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Manifest.cs @@ -0,0 +1,10 @@ +using OrchardCore.Modules.Manifest; + +[assembly: Module( + Name = "Security", + Author = ManifestConstants.OrchardCoreTeam, + Website = ManifestConstants.OrchardCoreWebsite, + Version = ManifestConstants.OrchardCoreVersion, + Description = "The Security module adds HTTP headers to follow security best practices.", + Category = "Security" +)] diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Options/ContentSecurityPolicyOriginValue.cs b/src/OrchardCore.Modules/OrchardCore.Security/Options/ContentSecurityPolicyOriginValue.cs new file mode 100644 index 00000000000..c55a98337f1 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Options/ContentSecurityPolicyOriginValue.cs @@ -0,0 +1,11 @@ +namespace OrchardCore.Security.Options +{ + public class ContentSecurityPolicyOriginValue + { + public const string Any = "*"; + + public const string None = "'none'"; + + public const string Self = "'self'"; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Options/ContentSecurityPolicyValue.cs b/src/OrchardCore.Modules/OrchardCore.Security/Options/ContentSecurityPolicyValue.cs new file mode 100644 index 00000000000..8259ec15e5d --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Options/ContentSecurityPolicyValue.cs @@ -0,0 +1,39 @@ +namespace OrchardCore.Security.Options +{ + public class ContentSecurityPolicyValue + { + public const string BaseUri = "base-uri"; + + public const string ChildSource = "child-src"; + + public const string ConnectSource = "connect-src"; + + public const string DefaultSource = "default-src"; + + public const string FontSource = "font-src"; + + public const string FormAction = "form-action"; + + public const string FrameAncestors = "frame-ancestors"; + + public const string FrameSource = "frame-src"; + + public const string ImageSource = "img-src"; + + public const string ManifestSource = "manifest-src"; + + public const string MediaSource = "media-src"; + + public const string ObjectSource = "object-src"; + + public const string ReportUri = "report-uri"; + + public const string Sandbox = "sandbox"; + + public const string ScriptSource = "script-src"; + + public const string StyleSource = "style-src"; + + public const string UpgradeInsecureRequests = "upgrade-insecure-requests"; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Options/ContentTypeOptionsValue.cs b/src/OrchardCore.Modules/OrchardCore.Security/Options/ContentTypeOptionsValue.cs new file mode 100644 index 00000000000..b2e6115bb8e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Options/ContentTypeOptionsValue.cs @@ -0,0 +1,7 @@ +namespace OrchardCore.Security.Options +{ + public class ContentTypeOptionsValue + { + public const string NoSniff = "nosniff"; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Options/PermissionsPolicyOriginValue.cs b/src/OrchardCore.Modules/OrchardCore.Security/Options/PermissionsPolicyOriginValue.cs new file mode 100644 index 00000000000..09f7d7585e8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Options/PermissionsPolicyOriginValue.cs @@ -0,0 +1,11 @@ +namespace OrchardCore.Security.Options +{ + public class PermissionsPolicyOriginValue + { + public const string Any = "*"; + + public const string None = "()"; + + public const string Self = "self"; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Options/PermissionsPolicyValue.cs b/src/OrchardCore.Modules/OrchardCore.Security/Options/PermissionsPolicyValue.cs new file mode 100644 index 00000000000..3203daadefd --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Options/PermissionsPolicyValue.cs @@ -0,0 +1,65 @@ +namespace OrchardCore.Security.Options +{ + public class PermissionsPolicyValue + { + public const string Accelerometer = "accelerometer"; + + public const string AmbientLightSensor = "ambient-light-sensor"; + + public const string Autoplay = "autoplay"; + + public const string Battery = "battery"; + + public const string Camera = "camera"; + + public const string DisplayCapture = "display-capture"; + + public const string DocumentDomain = "document-domain"; + + public const string EncryptedMedia = "encrypted-media"; + + public const string FullScreen = "fullscreen"; + + public const string GamePad = "gamepad"; + + public const string Geolocation = "geolocation"; + + public const string Gyroscope = "gyroscope"; + + public const string LayoutAnimations = "layout-animations"; + + public const string LegacyImageFormat = "legacy-image-format"; + + public const string Magnetometer = "magnetometer"; + + public const string Microphone = "microphone"; + + public const string Midi = "midi"; + + public const string OversizedImages = "oversized-images"; + + public const string Payment = "payment"; + + public const string PictureInPicture = "picture-in-picture"; + + public const string PublicKeyRetrieval = "publickey-credentials-get"; + + public const string Push = "push"; + + public const string SpeakerSelection = "speaker-selection"; + + public const string ScreenWakeLock = "screen-wake-lock"; + + public const string SyncXhr = "sync-xhr"; + + public const string UnoptimizedImages = "unoptimized-images"; + + public const string UnsizedMedia = "unsized-media"; + + public const string Usb = "usb"; + + public const string WebShare = "web-share"; + + public const string WebXR = "xr-spatial-tracking"; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Options/ReferrerPolicyValue.cs b/src/OrchardCore.Modules/OrchardCore.Security/Options/ReferrerPolicyValue.cs new file mode 100644 index 00000000000..b2fc88e7c4c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Options/ReferrerPolicyValue.cs @@ -0,0 +1,21 @@ +namespace OrchardCore.Security.Options +{ + public class ReferrerPolicyValue + { + public const string NoReferrer = "no-referrer"; + + public const string NoReferrerWhenDowngrade = "no-referrer-when-downgrade"; + + public const string Origin = "origin"; + + public const string OriginWhenCrossOrigin = "origin-when-cross-origin"; + + public const string SameOrigin = "same-origin"; + + public const string StrictOrigin = "strict-origin"; + + public const string StrictOriginWhenCrossOrigin = "strict-origin-when-cross-origin"; + + public const string UnsafeUrl = "unsafe-url"; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Options/SecurityHeadersOptions.cs b/src/OrchardCore.Modules/OrchardCore.Security/Options/SecurityHeadersOptions.cs new file mode 100644 index 00000000000..3b8665a2202 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Options/SecurityHeadersOptions.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OrchardCore.Security.Services; + +namespace OrchardCore.Security.Options +{ + public class SecurityHeadersOptions + { + public SecurityHeadersOptions() + { + HeaderPolicyProviders = new List + { + new ContentSecurityPolicyHeaderPolicyProvider { Options = this }, + new ContentTypeOptionsHeaderPolicyProvider { Options = this }, + new PermissionsHeaderPolicyProvider { Options = this }, + new ReferrerHeaderPolicyProvider { Options = this } + }; + } + + public string[] ContentSecurityPolicy { get; set; } = SecurityHeaderDefaults.ContentSecurityPolicy; + + public string ContentTypeOptions { get; set; } = SecurityHeaderDefaults.ContentTypeOptions; + + public string[] PermissionsPolicy { get; set; } = SecurityHeaderDefaults.PermissionsPolicy; + + public string ReferrerPolicy { get; set; } = SecurityHeaderDefaults.ReferrerPolicy; + + public IList HeaderPolicyProviders { get; set; } + + public SecurityHeadersOptions AddContentSecurityPolicy(Dictionary policies) + { + ContentSecurityPolicy = policies + .Where(kvp => kvp.Value != null || + kvp.Key == ContentSecurityPolicyValue.Sandbox || + kvp.Key == ContentSecurityPolicyValue.UpgradeInsecureRequests) + .Select(kvp => kvp.Key + (kvp.Value != null ? " " + kvp.Value : String.Empty)) + .ToArray(); + + return this; + } + + public SecurityHeadersOptions AddContentSecurityPolicy(params string[] policies) + { + ContentSecurityPolicy = policies; + + return this; + } + + public SecurityHeadersOptions AddContentTypeOptions() + { + ContentTypeOptions = ContentTypeOptionsValue.NoSniff; + + return this; + } + + public SecurityHeadersOptions AddPermissionsPolicy(IDictionary policies) + { + PermissionsPolicy = policies + .Where(kvp => kvp.Value != PermissionsPolicyOriginValue.None) + .Select(kvp => kvp.Key + "=" + kvp.Value) + .ToArray(); + + return this; + } + + public SecurityHeadersOptions AddReferrerPolicy(string policy) + { + ReferrerPolicy = policy; + + return this; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/OrchardCore.Security.csproj b/src/OrchardCore.Modules/OrchardCore.Security/OrchardCore.Security.csproj new file mode 100644 index 00000000000..e0c17ffda29 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/OrchardCore.Security.csproj @@ -0,0 +1,25 @@ + + + + true + + OrchardCore Security + + $(OCCMSDescription) + + The Security module adds HTTP headers to follow security best practices. + + $(PackageTags) OrchardCoreCMS + + + + + + + + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Permissions.cs b/src/OrchardCore.Modules/OrchardCore.Security/Permissions.cs new file mode 100644 index 00000000000..1187ec358a1 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Permissions.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using OrchardCore.Security.Permissions; + +namespace OrchardCore.Security +{ + public class SecurityPermissions : IPermissionProvider + { + public static readonly Permission ManageSecurityHeadersSettings = new("ManageSecurityHeadersSettings", "Manage Security Headers Settings"); + + public Task> GetPermissionsAsync() + => Task.FromResult(new[] { ManageSecurityHeadersSettings }.AsEnumerable()); + + public IEnumerable GetDefaultStereotypes() + => new[] + { + new PermissionStereotype + { + Name = "Administrator", + Permissions = new[] { ManageSecurityHeadersSettings } + }, + }; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/SecurityHeaderDefaults.cs b/src/OrchardCore.Modules/OrchardCore.Security/SecurityHeaderDefaults.cs new file mode 100644 index 00000000000..e0c9d21a94e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/SecurityHeaderDefaults.cs @@ -0,0 +1,68 @@ +using System; +using OrchardCore.Security.Options; + +namespace OrchardCore.Security +{ + public static class SecurityHeaderDefaults + { + internal static readonly char PoliciesSeparator = ','; + internal static readonly string[] ContentSecurityPolicyNames = new[] + { + ContentSecurityPolicyValue.BaseUri, + ContentSecurityPolicyValue.ChildSource, + ContentSecurityPolicyValue.ConnectSource, + ContentSecurityPolicyValue.DefaultSource, + ContentSecurityPolicyValue.FontSource, + ContentSecurityPolicyValue.FormAction, + ContentSecurityPolicyValue.FrameAncestors, + ContentSecurityPolicyValue.FrameSource, + ContentSecurityPolicyValue.ImageSource, + ContentSecurityPolicyValue.ManifestSource, + ContentSecurityPolicyValue.MediaSource, + ContentSecurityPolicyValue.ObjectSource, + ContentSecurityPolicyValue.ScriptSource, + ContentSecurityPolicyValue.StyleSource, + ContentSecurityPolicyValue.Sandbox + }; + internal static readonly string[] PermissionsPolicyNames = new[] + { + PermissionsPolicyValue.Accelerometer, + PermissionsPolicyValue.AmbientLightSensor, + PermissionsPolicyValue.Autoplay, + PermissionsPolicyValue.Battery, + PermissionsPolicyValue.Camera, + PermissionsPolicyValue.DisplayCapture, + PermissionsPolicyValue.DocumentDomain, + PermissionsPolicyValue.EncryptedMedia, + PermissionsPolicyValue.FullScreen, + PermissionsPolicyValue.GamePad, + PermissionsPolicyValue.Geolocation, + PermissionsPolicyValue.Gyroscope, + PermissionsPolicyValue.LayoutAnimations, + PermissionsPolicyValue.LegacyImageFormat, + PermissionsPolicyValue.Magnetometer, + PermissionsPolicyValue.Microphone, + PermissionsPolicyValue.Midi, + PermissionsPolicyValue.OversizedImages, + PermissionsPolicyValue.Payment, + PermissionsPolicyValue.PictureInPicture, + PermissionsPolicyValue.PublicKeyRetrieval, + PermissionsPolicyValue.SpeakerSelection, + PermissionsPolicyValue.ScreenWakeLock, + PermissionsPolicyValue.SyncXhr, + PermissionsPolicyValue.UnoptimizedImages, + PermissionsPolicyValue.UnsizedMedia, + PermissionsPolicyValue.Usb, + PermissionsPolicyValue.WebShare, + PermissionsPolicyValue.WebXR + }; + + public static string[] ContentSecurityPolicy = Array.Empty(); + + public static readonly string ContentTypeOptions = ContentTypeOptionsValue.NoSniff; + + public static string[] PermissionsPolicy = Array.Empty(); + + public static readonly string ReferrerPolicy = ReferrerPolicyValue.NoReferrer; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/ContentSecurityPolicyHeaderPolicyProvider.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/ContentSecurityPolicyHeaderPolicyProvider.cs new file mode 100644 index 00000000000..d412c54f674 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/ContentSecurityPolicyHeaderPolicyProvider.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace OrchardCore.Security.Services +{ + public class ContentSecurityPolicyHeaderPolicyProvider : HeaderPolicyProvider + { + private string _policy; + + public override void InitializePolicy() + { + if (Options.ContentSecurityPolicy.Length > 0) + { + _policy = String.Join(SecurityHeaderDefaults.PoliciesSeparator, Options.ContentSecurityPolicy); + } + } + + public override void ApplyPolicy(HttpContext httpContext) + { + if (_policy != null) + { + httpContext.Response.Headers[SecurityHeaderNames.ContentSecurityPolicy] = _policy; + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/ContentTypeOptionsHeaderPolicyProvider.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/ContentTypeOptionsHeaderPolicyProvider.cs new file mode 100644 index 00000000000..59e6e8620ab --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/ContentTypeOptionsHeaderPolicyProvider.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; + +namespace OrchardCore.Security.Services +{ + public class ContentTypeOptionsHeaderPolicyProvider : HeaderPolicyProvider + { + public override void ApplyPolicy(HttpContext httpContext) + { + httpContext.Response.Headers[SecurityHeaderNames.XContentTypeOptions] = Options.ContentTypeOptions; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/HeaderPolicyProvider.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/HeaderPolicyProvider.cs new file mode 100644 index 00000000000..ef477dd612e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/HeaderPolicyProvider.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Http; +using OrchardCore.Security.Options; + +namespace OrchardCore.Security.Services +{ + public abstract class HeaderPolicyProvider : IHeaderPolicyProvider + { + public SecurityHeadersOptions Options { get; set; } + + public virtual void InitializePolicy() + { + // This is intentionally empty, only header policy provider(s) that need an initialization should override this + } + + public abstract void ApplyPolicy(HttpContext httpContext); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/IHeaderPolicyProvider.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/IHeaderPolicyProvider.cs new file mode 100644 index 00000000000..e07cb91319e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/IHeaderPolicyProvider.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Http; + +namespace OrchardCore.Security.Services +{ + public interface IHeaderPolicyProvider + { + void InitializePolicy(); + + void ApplyPolicy(HttpContext httpContext); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/ISecurityService.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/ISecurityService.cs new file mode 100644 index 00000000000..e58a9c10cc8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/ISecurityService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using OrchardCore.Security.Settings; + +namespace OrchardCore.Security.Services +{ + public interface ISecurityService + { + Task GetSettingsAsync(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/PermissionsHeaderPolicyProvider.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/PermissionsHeaderPolicyProvider.cs new file mode 100644 index 00000000000..e1334a018b5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/PermissionsHeaderPolicyProvider.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace OrchardCore.Security.Services +{ + public class PermissionsHeaderPolicyProvider : HeaderPolicyProvider + { + private string _policy; + + public override void InitializePolicy() + { + if (Options.PermissionsPolicy.Length > 0) + { + _policy = String.Join(SecurityHeaderDefaults.PoliciesSeparator, Options.PermissionsPolicy); + } + } + + public override void ApplyPolicy(HttpContext httpContext) + { + if (_policy != null) + { + httpContext.Response.Headers[SecurityHeaderNames.PermissionsPolicy] = _policy; + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/ReferrerHeaderPolicyProvider.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/ReferrerHeaderPolicyProvider.cs new file mode 100644 index 00000000000..a3f3adf8873 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/ReferrerHeaderPolicyProvider.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; + +namespace OrchardCore.Security.Services +{ + public class ReferrerHeaderPolicyProvider : HeaderPolicyProvider + { + public override void ApplyPolicy(HttpContext httpContext) + { + httpContext.Response.Headers[SecurityHeaderNames.ReferrerPolicy] = Options.ReferrerPolicy; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/SecurityHeadersMiddleware.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/SecurityHeadersMiddleware.cs new file mode 100644 index 00000000000..5558cb4e5b0 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/SecurityHeadersMiddleware.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using OrchardCore.Security.Options; + +namespace OrchardCore.Security.Services +{ + public class SecurityHeadersMiddleware + { + private readonly SecurityHeadersOptions _options; + private readonly RequestDelegate _next; + + public SecurityHeadersMiddleware(SecurityHeadersOptions options, RequestDelegate next) + { + _options = options; + _next = next; + + foreach (var provider in _options.HeaderPolicyProviders) + { + provider.InitializePolicy(); + } + } + + public Task Invoke(HttpContext context) + { + foreach (var provider in _options.HeaderPolicyProviders) + { + provider.ApplyPolicy(context); + } + + return _next.Invoke(context); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/SecurityService.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/SecurityService.cs new file mode 100644 index 00000000000..c2a6a0c2bd3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/SecurityService.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using OrchardCore.Entities; +using OrchardCore.Security.Settings; +using OrchardCore.Settings; + +namespace OrchardCore.Security.Services +{ + public class SecurityService : ISecurityService + { + private readonly ISiteService _siteService; + + public SecurityService(ISiteService siteService) + { + _siteService = siteService; + } + + public async Task GetSettingsAsync() + { + var securityHeadersSettings = await _siteService.GetSiteSettingsAsync(); + + return securityHeadersSettings.As(); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Services/SecuritySettingsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Security/Services/SecuritySettingsConfiguration.cs new file mode 100644 index 00000000000..7d155643fa7 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Services/SecuritySettingsConfiguration.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Security.Settings; + +namespace OrchardCore.Security.Services +{ + public class SecuritySettingsConfiguration : IConfigureOptions + { + private readonly ISecurityService _securityService; + + public SecuritySettingsConfiguration(ISecurityService securityService) + => _securityService = securityService; + + public void Configure(SecuritySettings options) + { + var securitySettings = _securityService.GetSettingsAsync() + .GetAwaiter().GetResult(); + + options.ContentSecurityPolicy = securitySettings.ContentSecurityPolicy; + options.ContentTypeOptions = securitySettings.ContentTypeOptions; + options.PermissionsPolicy = securitySettings.PermissionsPolicy; + options.ReferrerPolicy = securitySettings.ReferrerPolicy; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Settings/SecuritySettings.cs b/src/OrchardCore.Modules/OrchardCore.Security/Settings/SecuritySettings.cs new file mode 100644 index 00000000000..8431c216e45 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Settings/SecuritySettings.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using OrchardCore.Security.Options; + +namespace OrchardCore.Security.Settings +{ + public class SecuritySettings + { + private Dictionary _contentSecurityPolicy = new(); + private Dictionary _permissionsPolicy = new(); + + public Dictionary ContentSecurityPolicy + { + get => _contentSecurityPolicy; + set + { + if (value == null) + { + return; + } + + // Exclude null values and clone the dictionary to not be shared by site settings and options instances. + _contentSecurityPolicy = value + .Where(kvp => kvp.Value != null || + kvp.Key == ContentSecurityPolicyValue.Sandbox || + kvp.Key == ContentSecurityPolicyValue.UpgradeInsecureRequests) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + if (_contentSecurityPolicy.TryGetValue(ContentSecurityPolicyValue.UpgradeInsecureRequests, out _)) + { + _contentSecurityPolicy[ContentSecurityPolicyValue.UpgradeInsecureRequests] = null; + } + } + } + + public string ContentTypeOptions { get; set; } = SecurityHeaderDefaults.ContentTypeOptions; + + public Dictionary PermissionsPolicy + { + get => _permissionsPolicy; + set + { + if (value == null) + { + return; + } + + // Exlude 'None' values and clone the dictionary to not be shared by site settings and options instances. + _permissionsPolicy = value + .Where(kvp => kvp.Value != PermissionsPolicyOriginValue.None) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + } + + public string ReferrerPolicy { get; set; } = SecurityHeaderDefaults.ReferrerPolicy; + + public bool FromConfiguration { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Security/Startup.cs new file mode 100644 index 00000000000..dcb81b83cb7 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Startup.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Modules; +using OrchardCore.Navigation; +using OrchardCore.Security.Drivers; +using OrchardCore.Security.Permissions; +using OrchardCore.Security.Services; +using OrchardCore.Security.Settings; +using OrchardCore.Settings; + +namespace OrchardCore.Security +{ + public class Startup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped, SecuritySettingsDisplayDriver>(); + services.AddScoped(); + + services.AddSingleton(); + + services.AddTransient, SecuritySettingsConfiguration>(); + } + + public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + var securityOptions = serviceProvider.GetRequiredService>().Value; + + builder.UseSecurityHeaders(options => + { + options + .AddContentSecurityPolicy(securityOptions.ContentSecurityPolicy) + .AddContentTypeOptions() + .AddPermissionsPolicy(securityOptions.PermissionsPolicy) + .AddReferrerPolicy(securityOptions.ReferrerPolicy); + }); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/ViewModels/SecuritySettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Security/ViewModels/SecuritySettingsViewModel.cs new file mode 100644 index 00000000000..4368a44e9bc --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/ViewModels/SecuritySettingsViewModel.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using OrchardCore.Security.Options; + +namespace OrchardCore.Security.ViewModels +{ + public class SecuritySettingsViewModel + { + private Dictionary _contentSecurityPolicy = new(); + private Dictionary _permissionsPolicy = new(); + + public Dictionary ContentSecurityPolicy + { + get => _contentSecurityPolicy; + set + { + // Populate all policy values for the editor (null if not provided). + _contentSecurityPolicy = SecurityHeaderDefaults.ContentSecurityPolicyNames + .ToDictionary(name => name, name => + value?.ContainsKey(name) ?? false + ? value[name] + : null); + } + } + + public bool EnableSandbox { get; set; } + + public bool UpgradeInsecureRequests { get; set; } + + public Dictionary PermissionsPolicy + { + get => _permissionsPolicy; + set + { + // Populate all policy values for the editor ('None' if not provided). + _permissionsPolicy = SecurityHeaderDefaults.PermissionsPolicyNames + .ToDictionary(name => name, name => + value?.ContainsKey(name) ?? false + ? value[name] + : PermissionsPolicyOriginValue.None); + } + } + + public string ReferrerPolicy { get; set; } + + [BindNever] + public bool FromConfiguration { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Security/Views/SecurityHeadersSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Security/Views/SecurityHeadersSettings.Edit.cshtml new file mode 100644 index 00000000000..8c27ab8938a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Security/Views/SecurityHeadersSettings.Edit.cshtml @@ -0,0 +1,329 @@ +@using Microsoft.AspNetCore.Mvc.Localization +@using OrchardCore.Security.Options +@using OrchardCore.Security.ViewModels +@model SecuritySettingsViewModel + +@functions +{ + public LocalizedHtmlString GetContentSecurityPolicyName(string value) + { + LocalizedHtmlString name = null; + switch (value) + { + case ContentSecurityPolicyValue.BaseUri: + name = T["Base URI"]; + break; + case ContentSecurityPolicyValue.ChildSource: + name = T["Child Source"]; + break; + case ContentSecurityPolicyValue.ConnectSource: + name = T["Connect Source"]; + break; + case ContentSecurityPolicyValue.DefaultSource: + name = T["Default Source"]; + break; + case ContentSecurityPolicyValue.FontSource: + name = T["Font Source"]; + break; + case ContentSecurityPolicyValue.FormAction: + name = T["Form Action"]; + break; + case ContentSecurityPolicyValue.FrameAncestors: + name = T["Frame Ancestors"]; + break; + case ContentSecurityPolicyValue.FrameSource: + name = T["Frame Source"]; + break; + case ContentSecurityPolicyValue.ImageSource: + name = T["Image Source"]; + break; + case ContentSecurityPolicyValue.ManifestSource: + name = T["Manifest Source"]; + break; + case ContentSecurityPolicyValue.MediaSource: + name = T["Media Source"]; + break; + case ContentSecurityPolicyValue.ObjectSource: + name = T["Object Source"]; + break; + case ContentSecurityPolicyValue.Sandbox: + name = T["Sandbox"]; + break; + case ContentSecurityPolicyValue.ScriptSource: + name = T["Script Source"]; + break; + case ContentSecurityPolicyValue.StyleSource: + name = T["Style Source"]; + break; + } + + return name; + } + + public LocalizedHtmlString GetPermissionPolicyName(string value) + { + LocalizedHtmlString name = null; + switch (value) + { + case PermissionsPolicyValue.Accelerometer: + name = T["Accelerometer"]; + break; + case PermissionsPolicyValue.AmbientLightSensor: + name = T["Ambient Light Sensor"]; + break; + case PermissionsPolicyValue.Autoplay: + name = T["Autoplay"]; + break; + case PermissionsPolicyValue.Battery: + name = T["Battery"]; + break; + case PermissionsPolicyValue.Camera: + name = T["Camera"]; + break; + case PermissionsPolicyValue.DisplayCapture: + name = T["Display Capture"]; + break; + case PermissionsPolicyValue.DocumentDomain: + name = T["Document Domain"]; + break; + case PermissionsPolicyValue.EncryptedMedia: + name = T["Encrypted Media"]; + break; + case PermissionsPolicyValue.FullScreen: + name = T["Full Screen"]; + break; + case PermissionsPolicyValue.GamePad: + name = T["Game Pad"]; + break; + case PermissionsPolicyValue.Geolocation: + name = T["Geolocation"]; + break; + case PermissionsPolicyValue.Gyroscope: + name = T["Gyroscope"]; + break; + case PermissionsPolicyValue.LayoutAnimations: + name = T["Layout Animations"]; + break; + case PermissionsPolicyValue.LegacyImageFormat: + name = T["Legacy ImageFormat"]; + break; + case PermissionsPolicyValue.Magnetometer: + name = T["Magnetometer"]; + break; + case PermissionsPolicyValue.Microphone: + name = T["Microphone"]; + break; + case PermissionsPolicyValue.Midi: + name = T["Midi"]; + break; + case PermissionsPolicyValue.OversizedImages: + name = T["Oversized Images"]; + break; + case PermissionsPolicyValue.Payment: + name = T["Payment"]; + break; + case PermissionsPolicyValue.PictureInPicture: + name = T["Picture In Picture"]; + break; + case PermissionsPolicyValue.PublicKeyRetrieval: + name = T["Public Key Retrieval"]; + break; + case PermissionsPolicyValue.Push: + name = T["Push"]; + break; + case PermissionsPolicyValue.SpeakerSelection: + name = T["Speaker Selection"]; + break; + case PermissionsPolicyValue.ScreenWakeLock: + name = T["Screen Wake Lock"]; + break; + case PermissionsPolicyValue.SyncXhr: + name = T["Synchronous XML Http Request"]; + break; + case PermissionsPolicyValue.UnoptimizedImages: + name = T["Unoptimized Images"]; + break; + case PermissionsPolicyValue.UnsizedMedia: + name = T["Unsized Media"]; + break; + case PermissionsPolicyValue.Usb: + name = T["USB"]; + break; + case PermissionsPolicyValue.WebShare: + name = T["Web Share"]; + break; + case PermissionsPolicyValue.WebXR: + name = T["Web XR"]; + break; + } + + return name; + } +} + +

+ @T["The current tenant will be reloaded when the settings are saved."] +

+ +@if (Model.FromConfiguration) +{ +

+ @T["The current settings are coming from configuration sources, saving the settings will affect the AdminSettings not the configuration."] +

+} + +
+ +
+
+ + + @T["Enables a sandbox for the requested resource similar to the