From 8d6aee740d13c82a1c11690ccda56f613897df54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Jan 2024 23:01:38 +0100 Subject: [PATCH] Add ContentSecurityPolicyAttribute. --- .../ContentSecurityPolicyDirectives.cs | 2 + .../IContentSecurityPolicyProvider.cs | 17 ++++- ...yAttributeContentSecurityPolicyProvider.cs | 73 +++++++++++++++++++ .../Security/OrchardCoreBuilderExtensions.cs | 2 +- ...lAttributeContentSecurityPolicyProvider.cs | 38 ---------- 5 files changed, 90 insertions(+), 42 deletions(-) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs delete mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs index 69b2e02d..81c0c321 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs @@ -22,6 +22,7 @@ public static class ContentSecurityPolicyDirectives public const string Sandbox = "sandbox"; public const string ScriptSrc = "script-src"; public const string StyleSrc = "style-src"; + public const string WorkerSrc = "worker-src"; public static class CommonValues { @@ -34,5 +35,6 @@ public static class CommonValues // These values represent allowed protocol schemes. public const string Https = "https:"; public const string Data = "data:"; + public const string Blob = "blob:"; } } diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs index 31073597..10c464ee 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs @@ -19,8 +19,19 @@ public interface IContentSecurityPolicyProvider public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context); /// - /// Returns the directive called or or an empty string. + /// Returns the first non-empty directive from the or or an empty + /// string. /// - public static string GetDirective(IDictionary securityPolicies, string name) => - securityPolicies.GetMaybe(name) ?? securityPolicies.GetMaybe(DefaultSrc) ?? string.Empty; + public static string GetDirective(IDictionary securityPolicies, params string[] names) + { + foreach (var name in names) + { + if (securityPolicies.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value)) + { + return value; + } + } + + return securityPolicies.GetMaybe(DefaultSrc) ?? string.Empty; + } } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs new file mode 100644 index 00000000..76e77df8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs @@ -0,0 +1,73 @@ +using Lombiq.HelpfulLibraries.AspNetCore.Security; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Threading.Tasks; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Indicates that the action's view should have the script-src: unsafe-eval content security policy directive. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class ScriptUnsafeEvalAttribute : ContentSecurityPolicyAttribute +{ + public ScriptUnsafeEvalAttribute() + : base(UnsafeEval, ScriptSrc) + { + } +} + +/// +/// Indicates that the action's view should have the provided content security policy directive. +/// +[AttributeUsage(AttributeTargets.Method)] +[SuppressMessage( + "Performance", + "CA1813:Avoid unsealed attributes", + Justification = $"Inherited by {nameof(ScriptUnsafeEvalAttribute)}.")] +public class ContentSecurityPolicyAttribute : Attribute +{ + /// + /// Gets the fallback chain of the directive, excluding . This is used to get the current + /// value. + /// + public string[] DirectiveNames { get; } + + /// + /// Gets the value to be added to the directive. The content is split into words and added to the current values + /// without repetition. + /// + public string DirectiveValue { get; } + + public ContentSecurityPolicyAttribute(string directiveValue, params string[] directiveNames) + { + DirectiveValue = directiveValue; + DirectiveNames = directiveNames; + } +} + +public class ContentSecurityPolicyAttributeContentSecurityPolicyProvider : IContentSecurityPolicyProvider +{ + public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) + { + if (context.RequestServices.GetService() is { ActionContext: { } actionContext } && + actionContext.ActionDescriptor is ControllerActionDescriptor actionDescriptor) + { + foreach (var attribute in actionDescriptor.MethodInfo.GetCustomAttributes()) + { + securityPolicies[ScriptSrc] = IContentSecurityPolicyProvider + .GetDirective(securityPolicies, attribute.DirectiveNames) + .MergeWordSets(attribute.DirectiveValue); + } + } + + return ValueTask.CompletedTask; + } +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 9ec38e3c..2cbed769 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -67,7 +67,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( services => services .AddContentSecurityPolicyProvider() .AddContentSecurityPolicyProvider() - .AddContentSecurityPolicyProvider() + .AddContentSecurityPolicyProvider() .ConfigureSessionCookieAlwaysSecure(), (app, _, serviceProvider) => { diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs deleted file mode 100644 index c3d21d3c..00000000 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Lombiq.HelpfulLibraries.AspNetCore.Security; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; -using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Indicates that the action's view should have the script-src: unsafe-eval content security policy directive. -/// -[AttributeUsage(AttributeTargets.Method)] -public sealed class ScriptUnsafeEvalAttribute : Attribute -{ -} - -public class ScriptUnsafeEvalAttributeContentSecurityPolicyProvider : IContentSecurityPolicyProvider -{ - public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) - { - if (context.RequestServices.GetService() is { ActionContext: { } actionContext } && - actionContext.ActionDescriptor is ControllerActionDescriptor actionDescriptor && - actionDescriptor.MethodInfo.GetCustomAttributes().Any()) - { - securityPolicies[ScriptSrc] = IContentSecurityPolicyProvider - .GetDirective(securityPolicies, ScriptSrc) - .MergeWordSets(UnsafeEval); - } - - return ValueTask.CompletedTask; - } -}