From dad5c9dccbb5095618bf8ff9db91c6e4d152b402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 14:41:23 +0100 Subject: [PATCH 01/86] Add constants for content security policy directives. --- .../ContentSecurityPolicyDirectives.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs new file mode 100644 index 00000000..ae0f80ae --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs @@ -0,0 +1,32 @@ +namespace Lombiq.HelpfulLibraries.AspNetCore.Security; + +/// +/// The Content-Security-Policy directives defined in the W3C +/// Recommendation. +/// +public static class ContentSecurityPolicyDirectives +{ + public const string BaseUri = "base-uri"; + public const string ChildSrc = "child-src"; + public const string ConnectSrc = "connect-src"; + public const string DefaultSrc = "default-src"; + public const string FontSrc = "font-src"; + public const string FormAction = "form-action"; + public const string FrameAncestors = "frame-ancestors"; + public const string FrameSrc = "frame-src"; + public const string ImgSrc = "img-src"; + public const string MediaSrc = "media-src"; + public const string ObjectSrc = "object-src"; + public const string PluginTypes = "plugin-types"; + public const string ReportUri = "report-uri"; + public const string Sandbox = "sandbox"; + public const string ScriptSrc = "script-src"; + public const string StyleSrc = "style-src"; + + public class CommonValues + { + public const string Self = "'self'"; + public const string None = "'none'"; + public const string Https = "https:"; // This value intentionally doesn't contain apostrophes. + } +} From c761c229b2e03dcbcf2abb32eb65ecc1abdb8136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 14:42:27 +0100 Subject: [PATCH 02/86] Add middleware extension method and extension point for building Content-Security-Policy header. --- .../Security/ApplicationBuilderExtensions.cs | 29 +++++++++++++++++++ .../IContentSecurityPolicyProvider.cs | 18 ++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs create mode 100644 Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs new file mode 100644 index 00000000..addd0bad --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -0,0 +1,29 @@ +using Lombiq.HelpfulLibraries.AspNetCore.Security; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; + +namespace Microsoft.AspNetCore.Builder; + +public static class ApplicationBuilderExtensions +{ + public static IApplicationBuilder AddContentSecurityPolicyHeader(this IApplicationBuilder app) => + app.Use(async (context, next) => + { + var securityPolicies = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [DefaultSrc] = CommonValues.Self, + }; + + foreach (var provider in context.RequestServices.GetService>()) + { + await provider.UpdateAsync(securityPolicies); + } + + var policy = string.Join("; ", securityPolicies.Select((key, value) => $"{key} {value}")); + context.Response.Headers.Add("Content-Security-Policy", policy); + + await next(); + }); +} diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs new file mode 100644 index 00000000..f2b75ba2 --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Builder; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.AspNetCore.Security; + +/// +/// A service for updating the dictionary that will be turned into the Content-Security-Policy header value by +/// . +/// +public interface IContentSecurityPolicyProvider +{ + /// + /// Updates the dictionary where the keys are the Content-Security-Policy + /// directives names and the values are the matching directive values. + /// + public ValueTask UpdateAsync(IDictionary securityPolicies); +} From 02fb71b8d0bc9ad0096df8aa0873d6864724e435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 14:42:36 +0100 Subject: [PATCH 03/86] Add documentation. --- Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md | 7 +++++++ Lombiq.HelpfulLibraries.AspNetCore/Readme.md | 1 + 2 files changed, 8 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md new file mode 100644 index 00000000..a407b771 --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md @@ -0,0 +1,7 @@ +# Lombiq Helpful Libraries - ASP.NET Core Libraries - Security + +## `Content-Security-Policy` + +- `ApplicationBuilderExtensions`: Contains the `AddContentSecurityPolicyHeader` extension method to add a middleware that provides the `Content-Security-Policy` header. +- `ContentSecurityPolicyDirectives`: The `Content-Security-Policy` directive names that are defined in the W3C [recommendation](https://www.w3.org/TR/CSP2/#directives) and some common values. +- `IContentSecurityPolicyProvider`: Interface for services that update the dictionary that will be turned into the `Content-Security-Policy` header value. \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Readme.md b/Lombiq.HelpfulLibraries.AspNetCore/Readme.md index 5db79d96..70461a41 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Readme.md +++ b/Lombiq.HelpfulLibraries.AspNetCore/Readme.md @@ -14,3 +14,4 @@ Please see the inline documentation of each piece of the API to learn more. - [Localization](Docs/Localization.md) - [Middlewares](Docs/Middlewares.md) - [MVC](Docs/Mvc.md) +- [Security](Docs/Security.md) From 93f32b64d0b54b4781874735b70389268479f3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 16:38:17 +0100 Subject: [PATCH 04/86] Allow inline scripts and rename extension method. --- .../Security/ApplicationBuilderExtensions.cs | 12 +++++++++++- .../Security/ContentSecurityPolicyDirectives.cs | 7 ++++++- .../Security/IContentSecurityPolicyProvider.cs | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index addd0bad..b393644b 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -8,14 +8,24 @@ namespace Microsoft.AspNetCore.Builder; public static class ApplicationBuilderExtensions { - public static IApplicationBuilder AddContentSecurityPolicyHeader(this IApplicationBuilder app) => + /// + /// Adds a middleware that supplies Content-Security-Policy header. It may be further expanded by registering + /// services that implement . + /// + /// If inline scripts are permitted by including the + public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicationBuilder app, bool allowInline = true) => app.Use(async (context, next) => { var securityPolicies = new Dictionary(StringComparer.OrdinalIgnoreCase) { + // Default value enforcing a same origin policy for all resources. [DefaultSrc] = CommonValues.Self, + // Needed for SVG images using "data:image/svg+xml,..." data URLs. + [ImgSrc] = $"{CommonValues.Self} {CommonValues.Data}", }; + if (allowInline) securityPolicies[ScriptSrc] = CommonValues.UnsafeInline; + foreach (var provider in context.RequestServices.GetService>()) { await provider.UpdateAsync(securityPolicies); diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs index ae0f80ae..74eef377 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs @@ -25,8 +25,13 @@ public static class ContentSecurityPolicyDirectives public class CommonValues { + // These values represent special words so they must be surrounded with apostrophes. public const string Self = "'self'"; public const string None = "'none'"; - public const string Https = "https:"; // This value intentionally doesn't contain apostrophes. + public const string UnsafeInline = "'unsafe-inline'"; + + // These values represent allowed protocol schemes. + public const string Https = "https:"; + public const string Data = "data:"; } } diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs index f2b75ba2..4e6b7b99 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs @@ -6,7 +6,7 @@ namespace Lombiq.HelpfulLibraries.AspNetCore.Security; /// /// A service for updating the dictionary that will be turned into the Content-Security-Policy header value by -/// . +/// . /// public interface IContentSecurityPolicyProvider { From d47d807b260dab5f4813bcf147bf1644f643d916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 18:14:58 +0100 Subject: [PATCH 05/86] Fix UseContentSecurityPolicyHeader for OC basic features. --- .../Security/ApplicationBuilderExtensions.cs | 17 ++++++++++++----- .../Security/ContentSecurityPolicyDirectives.cs | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index b393644b..f596b03c 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; namespace Microsoft.AspNetCore.Builder; @@ -12,19 +13,25 @@ public static class ApplicationBuilderExtensions /// Adds a middleware that supplies Content-Security-Policy header. It may be further expanded by registering /// services that implement . /// - /// If inline scripts are permitted by including the - public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicationBuilder app, bool allowInline = true) => + /// If then inline scripts and styles are permitted. + public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicationBuilder app, bool allowInline) => app.Use(async (context, next) => { var securityPolicies = new Dictionary(StringComparer.OrdinalIgnoreCase) { // Default value enforcing a same origin policy for all resources. - [DefaultSrc] = CommonValues.Self, + [DefaultSrc] = Self, // Needed for SVG images using "data:image/svg+xml,..." data URLs. - [ImgSrc] = $"{CommonValues.Self} {CommonValues.Data}", + [ImgSrc] = $"{Self} {Data}", }; - if (allowInline) securityPolicies[ScriptSrc] = CommonValues.UnsafeInline; + // Orchard Core setup will fail without 'unsafe-inline'. Additionally, it's almost guaranteed that some page + // will contain non-precompiled Vue.js code from built-in OC features and that requires 'unsafe-eval'. + if (allowInline) + { + securityPolicies[ScriptSrc] = $"{Self} {UnsafeInline} {UnsafeEval}"; + securityPolicies[StyleSrc] = $"{Self} {UnsafeInline}"; + } foreach (var provider in context.RequestServices.GetService>()) { diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs index 74eef377..44dfafd6 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs @@ -29,6 +29,7 @@ public class CommonValues public const string Self = "'self'"; public const string None = "'none'"; public const string UnsafeInline = "'unsafe-inline'"; + public const string UnsafeEval = "'unsafe-eval'"; // These values represent allowed protocol schemes. public const string Https = "https:"; From 1226806586d31e107d16230bd16ed883e49ba301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 18:37:50 +0100 Subject: [PATCH 06/86] Add AntiClickjackingContentSecurityPolicyProvider. --- .../Docs/Security.md | 3 ++- ...ickjackingContentSecurityPolicyProvider.cs | 17 +++++++++++++++ .../Security/ApplicationBuilderExtensions.cs | 1 + .../Security/ServiceCollectionExtensions.cs | 21 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs create mode 100644 Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md index a407b771..52fb7b84 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md +++ b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md @@ -4,4 +4,5 @@ - `ApplicationBuilderExtensions`: Contains the `AddContentSecurityPolicyHeader` extension method to add a middleware that provides the `Content-Security-Policy` header. - `ContentSecurityPolicyDirectives`: The `Content-Security-Policy` directive names that are defined in the W3C [recommendation](https://www.w3.org/TR/CSP2/#directives) and some common values. -- `IContentSecurityPolicyProvider`: Interface for services that update the dictionary that will be turned into the `Content-Security-Policy` header value. \ No newline at end of file +- `IContentSecurityPolicyProvider`: Interface for services that update the dictionary that will be turned into the `Content-Security-Policy` header value. +- `AntiClickjackingContentSecurityPolicyProvider`: An optional policy provider that prevents clickjacking by including the `frame-ancestors 'self'` directive. diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs new file mode 100644 index 00000000..08a3854c --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; + +namespace Lombiq.HelpfulLibraries.AspNetCore.Security; + +public class AntiClickjackingContentSecurityPolicyProvider : IContentSecurityPolicyProvider +{ + public ValueTask UpdateAsync(IDictionary securityPolicies) + { + securityPolicies[FrameAncestors] = Self; + + return ValueTask.CompletedTask; + } +} diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index f596b03c..45b7e33c 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; + using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..972ce9c8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using Lombiq.HelpfulLibraries.AspNetCore.Security; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + /// + /// Registers a Content Security Policy provider that implements and + /// will be used by . + /// + public static IServiceCollection AddContentSecurityPolicyProvider(this IServiceCollection services) + where TProvider : class, IContentSecurityPolicyProvider => + services.AddScoped(); + + /// + /// Registers . + /// + public static IServiceCollection AddAntiClickjackingContentSecurityPolicyProvider(this IServiceCollection services) => + services.AddContentSecurityPolicyProvider(); +} From 339741e4e024c01ab7e49a8f429e4092a3b8b70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 21:02:49 +0100 Subject: [PATCH 07/86] Secure anti forgery token. --- .../Security/OrchardCoreBuilderExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs new file mode 100644 index 00000000..25298774 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class SecurityOrchardCoreBuilderExtensions +{ + /// + /// Configures the anti-forgery token to be always secure. With this configuration the token won't work in an HTTP + /// environment so make sure that HTTPS redirection is enabled. + /// + public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCoreBuilder builder) => + builder.ConfigureServices((services, _) => + services.Configure(options => options.Cookie.SecurePolicy = CookieSecurePolicy.Always)); +} From c3beb82dab7be68a08a848b75ed0824b8e8b3e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 21:04:36 +0100 Subject: [PATCH 08/86] Change default SameSite attribute of SetCookieForever. --- .../Extensions/CookieHttpContextExtensions .cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/CookieHttpContextExtensions .cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/CookieHttpContextExtensions .cs index a23bd9e7..759ce23f 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/CookieHttpContextExtensions .cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/CookieHttpContextExtensions .cs @@ -7,12 +7,13 @@ public static class CookieHttpContextExtensions /// /// Sets the cookie with the given name with a maximal expiration time. /// - public static void SetCookieForever(this HttpContext httpContext, string name, string value) => + public static void SetCookieForever(this HttpContext httpContext, string name, string value, SameSiteMode sameSite = SameSiteMode.Lax) => httpContext.Response.Cookies.Append(name, value, new CookieOptions { Expires = DateTimeOffset.MaxValue, Secure = true, HttpOnly = true, + SameSite = sameSite, }); /// From e547d69eae2c609eaa467828a8d2188f30ff080c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 21:14:41 +0100 Subject: [PATCH 09/86] Also pass in HttpContext. --- .../AntiClickjackingContentSecurityPolicyProvider.cs | 5 +++-- .../Security/ApplicationBuilderExtensions.cs | 2 +- .../Security/IContentSecurityPolicyProvider.cs | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs index 08a3854c..7c9a0568 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; using System.Threading.Tasks; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; @@ -8,7 +9,7 @@ namespace Lombiq.HelpfulLibraries.AspNetCore.Security; public class AntiClickjackingContentSecurityPolicyProvider : IContentSecurityPolicyProvider { - public ValueTask UpdateAsync(IDictionary securityPolicies) + public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { securityPolicies[FrameAncestors] = Self; diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 45b7e33c..4c60c5b7 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -36,7 +36,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati foreach (var provider in context.RequestServices.GetService>()) { - await provider.UpdateAsync(securityPolicies); + await provider.UpdateAsync(securityPolicies, context); } var policy = string.Join("; ", securityPolicies.Select((key, value) => $"{key} {value}")); diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs index 4e6b7b99..20ab5c86 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using System.Collections.Generic; using System.Threading.Tasks; @@ -14,5 +15,5 @@ public interface IContentSecurityPolicyProvider /// Updates the dictionary where the keys are the Content-Security-Policy /// directives names and the values are the matching directive values. /// - public ValueTask UpdateAsync(IDictionary securityPolicies); + public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context); } From 9b14fdd80bb82251d95710c03a6c014214a13cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 22 Dec 2023 22:14:11 +0100 Subject: [PATCH 10/86] Config common directives. --- .../Security/ApplicationBuilderExtensions.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 4c60c5b7..520835e1 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -20,10 +20,16 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati { var securityPolicies = new Dictionary(StringComparer.OrdinalIgnoreCase) { - // Default value enforcing a same origin policy for all resources. + // Default values enforcing a same origin policy for all resources. + [BaseUri] = Self, [DefaultSrc] = Self, + [FrameSrc] = Self, + [ScriptSrc] = Self, + [StyleSrc] = Self, // Needed for SVG images using "data:image/svg+xml,..." data URLs. [ImgSrc] = $"{Self} {Data}", + // Modern sites shouldn't need , , and elements. + [ObjectSrc] = None, }; // Orchard Core setup will fail without 'unsafe-inline'. Additionally, it's almost guaranteed that some page From b3f9572fb4a628456c98542b051eb24841df2116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 26 Dec 2023 01:40:39 +0100 Subject: [PATCH 11/86] Add spelling word "clickjacking". --- .github/actions/spelling/allow/security.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/actions/spelling/allow/security.txt diff --git a/.github/actions/spelling/allow/security.txt b/.github/actions/spelling/allow/security.txt new file mode 100644 index 00000000..5457a44e --- /dev/null +++ b/.github/actions/spelling/allow/security.txt @@ -0,0 +1 @@ +clickjacking \ No newline at end of file From 2358851e977dd98c6ba3eb058bf2b10e664cdab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 26 Dec 2023 01:41:04 +0100 Subject: [PATCH 12/86] Add missing newline. --- .github/actions/spelling/allow/security.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow/security.txt b/.github/actions/spelling/allow/security.txt index 5457a44e..a27c363c 100644 --- a/.github/actions/spelling/allow/security.txt +++ b/.github/actions/spelling/allow/security.txt @@ -1 +1 @@ -clickjacking \ No newline at end of file +clickjacking From 32ddf0146125b5482959971bf75b829d82505504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 26 Dec 2023 01:54:53 +0100 Subject: [PATCH 13/86] Fix CA1052. --- .../Security/ContentSecurityPolicyDirectives.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs index 44dfafd6..69b2e02d 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ContentSecurityPolicyDirectives.cs @@ -23,7 +23,7 @@ public static class ContentSecurityPolicyDirectives public const string ScriptSrc = "script-src"; public const string StyleSrc = "style-src"; - public class CommonValues + public static class CommonValues { // These values represent special words so they must be surrounded with apostrophes. public const string Self = "'self'"; From fcb78ee837fbb95f1bd80e29bb01b1456fbd658b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 26 Dec 2023 02:26:25 +0100 Subject: [PATCH 14/86] Add form-action directive. --- .../Security/ApplicationBuilderExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 520835e1..902c84c5 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -26,6 +26,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati [FrameSrc] = Self, [ScriptSrc] = Self, [StyleSrc] = Self, + [FormAction] = Self, // Needed for SVG images using "data:image/svg+xml,..." data URLs. [ImgSrc] = $"{Self} {Data}", // Modern sites shouldn't need , , and elements. From deb91baa94dc21cc72ae824058d5a9ae4cba8a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 26 Dec 2023 20:33:13 +0100 Subject: [PATCH 15/86] Add ConfigureSessionCookieAlwaysSecure. --- .../Security/ServiceCollectionExtensions.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs index 972ce9c8..87c8a7b1 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Lombiq.HelpfulLibraries.AspNetCore.Security; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; namespace Microsoft.Extensions.DependencyInjection; @@ -18,4 +19,11 @@ public static IServiceCollection AddContentSecurityPolicyProvider(thi /// public static IServiceCollection AddAntiClickjackingContentSecurityPolicyProvider(this IServiceCollection services) => services.AddContentSecurityPolicyProvider(); + + /// + /// Configures the session cookie to be always secure. With this configuration the token won't work in an HTTP + /// environment so make sure that HTTPS redirection is enabled. + /// + public static IServiceCollection ConfigureSessionCookieAlwaysSecure(this IServiceCollection services) => + services.Configure(options => options.Cookie.SecurePolicy = CookieSecurePolicy.Always); } From 4f33eb4805ac500bc926909603272094f06fe9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 26 Dec 2023 21:39:03 +0100 Subject: [PATCH 16/86] Add UseContentTypeOptionsHeader. --- .../Security/ApplicationBuilderExtensions.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 902c84c5..0cacd9a3 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -49,6 +49,24 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati var policy = string.Join("; ", securityPolicies.Select((key, value) => $"{key} {value}")); context.Response.Headers.Add("Content-Security-Policy", policy); + await next(); + }); + + /// + /// Adds a middleware that sets the X-Content-Type-Options header to nosniff. + /// + /// + /// The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of + /// Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response + /// body to be interpreted and displayed as a content type other than the declared content type. Current (early + /// 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing + /// MIME-sniffing. + /// + public static IApplicationBuilder UseContentTypeOptionsHeader(this IApplicationBuilder app) => + app.Use(async (context, next) => + { + context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); + await next(); }); } From 4f8b5e6e0ef867177eac3aa0c52b1b11966bd31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 00:09:51 +0100 Subject: [PATCH 17/86] Add defaults extension methods. --- .../Security/OrchardCoreBuilderExtensions.cs | 10 ++++++++++ .../SecurityApplicationBuilderExtensions.cs | 14 ++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 25298774..cce4555d 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder; namespace Microsoft.Extensions.DependencyInjection; @@ -12,4 +13,13 @@ public static class SecurityOrchardCoreBuilderExtensions public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCoreBuilder builder) => builder.ConfigureServices((services, _) => services.Configure(options => options.Cookie.SecurePolicy = CookieSecurePolicy.Always)); + + /// + /// Provides some default security configuration for Orchard Core. Use it in conjunction with . + /// + public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuilder builder) => builder + .ConfigureServices(services => services.AddAntiClickjackingContentSecurityPolicyProvider()) + .ConfigureAntiForgeryAlwaysSecure() + .AddTenantFeatures("OrchardCore.Diagnostics"); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs new file mode 100644 index 00000000..72050d71 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder; + +public static class SecurityApplicationBuilderExtensions +{ + /// + /// Provides some default security middlewares for Orchard Core. Use it in conjunction with . + /// + public static IApplicationBuilder UseSecurityDefaults(this IApplicationBuilder app) => app + .UseContentSecurityPolicyHeader(allowInline: true) + .UseContentTypeOptionsHeader(); +} From 4df453c996a37baeb43c0c8bb5707f8e5b4890d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 00:15:38 +0100 Subject: [PATCH 18/86] Fix usings. --- .../Security/OrchardCoreBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index cce4555d..49e04a69 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Antiforgery; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; namespace Microsoft.Extensions.DependencyInjection; From c87124e88eeff895f756abc04b0eb97ea166cbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 00:54:38 +0100 Subject: [PATCH 19/86] Delete .github/actions/spelling/allow/security.txt --- .github/actions/spelling/allow/security.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/actions/spelling/allow/security.txt diff --git a/.github/actions/spelling/allow/security.txt b/.github/actions/spelling/allow/security.txt deleted file mode 100644 index a27c363c..00000000 --- a/.github/actions/spelling/allow/security.txt +++ /dev/null @@ -1 +0,0 @@ -clickjacking From d01ce294ddee5be1500dd12ddce632484187065c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 00:55:02 +0100 Subject: [PATCH 20/86] Update Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- .../Security/ApplicationBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 0cacd9a3..67fd6be0 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -11,8 +11,8 @@ namespace Microsoft.AspNetCore.Builder; public static class ApplicationBuilderExtensions { /// - /// Adds a middleware that supplies Content-Security-Policy header. It may be further expanded by registering - /// services that implement . + /// Adds a middleware that supplies the Content-Security-Policy header. It may be further expanded by + /// registering services that implement . /// /// If then inline scripts and styles are permitted. public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicationBuilder app, bool allowInline) => From 9633f9aac283b0bf7fb277e9d55fd976b0060c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 00:56:01 +0100 Subject: [PATCH 21/86] Update Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- .../Security/ApplicationBuilderExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 67fd6be0..68f4d909 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -58,9 +58,9 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati /// /// The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of /// Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response - /// body to be interpreted and displayed as a content type other than the declared content type. Current (early - /// 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing - /// MIME-sniffing. + /// body to be interpreted and displayed as a content type other than the declared content type. Current (younger + /// than early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than + /// performing MIME-sniffing. /// public static IApplicationBuilder UseContentTypeOptionsHeader(this IApplicationBuilder app) => app.Use(async (context, next) => From cfba7e21bdee5e756ab8765d4368c176ca4e63b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 01:33:35 +0100 Subject: [PATCH 22/86] Fix ConfigureSecurityDefaults. --- .../Security/OrchardCoreBuilderExtensions.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 49e04a69..e7e99d3c 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -18,8 +18,11 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo /// Provides some default security configuration for Orchard Core. Use it in conjunction with . /// - public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuilder builder) => builder - .ConfigureServices(services => services.AddAntiClickjackingContentSecurityPolicyProvider()) - .ConfigureAntiForgeryAlwaysSecure() - .AddTenantFeatures("OrchardCore.Diagnostics"); + public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuilder builder) + { + builder.ApplicationServices.AddAntiClickjackingContentSecurityPolicyProvider(); + return builder + .ConfigureAntiForgeryAlwaysSecure() + .AddTenantFeatures("OrchardCore.Diagnostics"); + } } From 826815df43ad80e718a8be76f6f790cabcaaf434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 01:59:14 +0100 Subject: [PATCH 23/86] Add more documentation for the default security extension methods. --- .../Security/OrchardCoreBuilderExtensions.cs | 23 ++++++++++++++++++- .../SecurityApplicationBuilderExtensions.cs | 14 +++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index e7e99d3c..bf4fdac4 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Antiforgery; +using Lombiq.HelpfulLibraries.AspNetCore.Security; +using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -18,6 +19,26 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo /// Provides some default security configuration for Orchard Core. Use it in conjunction with . /// + /// + /// + /// + /// + /// Add to prevent clickjacking. + /// + /// + /// + /// + /// Call to make the anti-forgery token secure. + /// + /// + /// + /// + /// Enable the OrchardCore.Diagnostics feature to provide custom error screens in production and + /// don't leak error information. + /// + /// + /// + /// public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuilder builder) { builder.ApplicationServices.AddAntiClickjackingContentSecurityPolicyProvider(); diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs index 72050d71..c97d7e44 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs @@ -8,6 +8,20 @@ public static class SecurityApplicationBuilderExtensions /// Provides some default security middlewares for Orchard Core. Use it in conjunction with . /// + /// + /// + /// + /// + /// Adds a middleware that supplies the Content-Security-Policy header. + /// + /// + /// + /// + /// Adds a middleware that supplies the X-Content-Type-Options: nosniff header. + /// + /// + /// + /// public static IApplicationBuilder UseSecurityDefaults(this IApplicationBuilder app) => app .UseContentSecurityPolicyHeader(allowInline: true) .UseContentTypeOptionsHeader(); From 242f4b4476b745ce95f163e185adc83005b638de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 13:15:15 +0100 Subject: [PATCH 24/86] Add InlineStartup and change ConfigureSecurityDefaults to use it. --- .../Security/ApplicationBuilderExtensions.cs | 7 ++- .../DependencyInjection/InlineStartup.cs | 43 +++++++++++++++++++ .../ServiceCollectionExtensions.cs | 26 +++++++++++ .../Security/OrchardCoreBuilderExtensions.cs | 18 +++++++- .../SecurityApplicationBuilderExtensions.cs | 28 ------------ 5 files changed, 92 insertions(+), 30 deletions(-) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/InlineStartup.cs delete mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 68f4d909..01c6f555 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -65,7 +65,12 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati public static IApplicationBuilder UseContentTypeOptionsHeader(this IApplicationBuilder app) => app.Use(async (context, next) => { - context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); + const string key = "X-Content-Type-Options"; + + if (!context.Response.Headers.ContainsKey(key)) + { + context.Response.Headers.Add(key, "nosniff"); + } await next(); }); diff --git a/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/InlineStartup.cs b/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/InlineStartup.cs new file mode 100644 index 00000000..2d45f6ca --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/InlineStartup.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Modules; +using System; + +namespace Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; + +/// +/// A startup class that invokes the delegates provided in the constructor. +/// +public class InlineStartup : StartupBase +{ + private readonly Action _configureServices; + private readonly Action _configure; + private readonly int _order; + + public override int Order => _order; + + public InlineStartup( + Action configureServices, + Action configure, + int order = 0) + : this(configureServices, (app, _, _) => configure(app), order) + { + } + + public InlineStartup( + Action configureServices, + Action configure = null, + int order = 0) + { + _configureServices = configureServices; + _configure = configure; + _order = order; + } + + public override void ConfigureServices(IServiceCollection services) => + _configureServices?.Invoke(services); + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => + _configure?.Invoke(app, routes, serviceProvider); +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/ServiceCollectionExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/ServiceCollectionExtensions.cs index 18e2ab9b..8a282561 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/ServiceCollectionExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,6 +1,10 @@ using Lombiq.HelpfulLibraries.Common.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using OrchardCore.Modules; +using System; namespace Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; @@ -15,4 +19,26 @@ public static void AddOrchardServices(this IServiceCollection services) services.AddLazyInjectionSupport(); services.TryAddTransient(typeof(IOrchardServices<>), typeof(OrchardServices<>)); } + + /// + /// Creates a new instance using the provided parameters, and adds it to the service + /// collection. + /// + public static IServiceCollection AddInlineStartup( + this IServiceCollection services, + Action configureServices, + Action configure, + int order = 0) => + services.AddSingleton(new InlineStartup(configureServices, configure, order)); + + /// + /// Creates a new instance using the provided parameters, and adds it to the service + /// collection. + /// + public static IServiceCollection AddInlineStartup( + this IServiceCollection services, + Action configureServices, + Action configure, + int order = 0) => + services.AddSingleton(new InlineStartup(configureServices, configure, order)); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index bf4fdac4..d5dd1a3a 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -1,4 +1,5 @@ using Lombiq.HelpfulLibraries.AspNetCore.Security; +using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -37,11 +38,26 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo /// don't leak error information. /// /// + /// + /// + /// Adds a middleware that supplies the Content-Security-Policy header. + /// + /// + /// + /// + /// Adds a middleware that supplies the X-Content-Type-Options: nosniff header. + /// + /// /// /// public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuilder builder) { - builder.ApplicationServices.AddAntiClickjackingContentSecurityPolicyProvider(); + builder.ApplicationServices.AddInlineStartup( + services => services.AddAntiClickjackingContentSecurityPolicyProvider(), + app => app + .UseContentSecurityPolicyHeader(allowInline: true) + .UseContentTypeOptionsHeader(), + order: 99); return builder .ConfigureAntiForgeryAlwaysSecure() .AddTenantFeatures("OrchardCore.Diagnostics"); diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs deleted file mode 100644 index c97d7e44..00000000 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/SecurityApplicationBuilderExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Builder; - -public static class SecurityApplicationBuilderExtensions -{ - /// - /// Provides some default security middlewares for Orchard Core. Use it in conjunction with . - /// - /// - /// - /// - /// - /// Adds a middleware that supplies the Content-Security-Policy header. - /// - /// - /// - /// - /// Adds a middleware that supplies the X-Content-Type-Options: nosniff header. - /// - /// - /// - /// - public static IApplicationBuilder UseSecurityDefaults(this IApplicationBuilder app) => app - .UseContentSecurityPolicyHeader(allowInline: true) - .UseContentTypeOptionsHeader(); -} From 02e314946aec72c375aec3ebeac8eafc3beb34ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 14:13:41 +0100 Subject: [PATCH 25/86] Rename UseContentTypeOptionsHeader to UseNosniffContentTypeOptionsHeader --- .../Security/ApplicationBuilderExtensions.cs | 2 +- .../Security/OrchardCoreBuilderExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 01c6f555..1f6dcef5 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -62,7 +62,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati /// than early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than /// performing MIME-sniffing. /// - public static IApplicationBuilder UseContentTypeOptionsHeader(this IApplicationBuilder app) => + public static IApplicationBuilder UseNosniffContentTypeOptionsHeader(this IApplicationBuilder app) => app.Use(async (context, next) => { const string key = "X-Content-Type-Options"; diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index d5dd1a3a..c5e4f8c1 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -56,7 +56,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuild services => services.AddAntiClickjackingContentSecurityPolicyProvider(), app => app .UseContentSecurityPolicyHeader(allowInline: true) - .UseContentTypeOptionsHeader(), + .UseNosniffContentTypeOptionsHeader(), order: 99); return builder .ConfigureAntiForgeryAlwaysSecure() From cf26dddb154d1a4015f1dada4ee2ff8ce6801c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 14:21:29 +0100 Subject: [PATCH 26/86] Update xmldoc to make it clear that it's a quote. --- .../Security/ApplicationBuilderExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 1f6dcef5..c2eb7511 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -56,11 +56,11 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati /// Adds a middleware that sets the X-Content-Type-Options header to nosniff. /// /// - /// The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of - /// Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response - /// body to be interpreted and displayed as a content type other than the declared content type. Current (younger - /// than early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than - /// performing MIME-sniffing. + /// As per the documentation: "The Anti-MIME-Sniffing + /// header X-Content-Type-Options was not set to ’nosniff’. This allows older versions of Internet Explorer and + /// Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted + /// and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions + /// of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing." /// public static IApplicationBuilder UseNosniffContentTypeOptionsHeader(this IApplicationBuilder app) => app.Use(async (context, next) => From c9bc0ae38bbf5e5c705a074b3c5d69d8c1d02f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 14:35:17 +0100 Subject: [PATCH 27/86] Update documentation. --- Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md | 5 +++++ Lombiq.HelpfulLibraries.OrchardCore/Readme.md | 1 + .../Security/OrchardCoreBuilderExtensions.cs | 3 +-- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md new file mode 100644 index 00000000..48b099ca --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md @@ -0,0 +1,5 @@ +# Lombiq Helpful Libraries - Orchard Core Libraries - Security + +## Extensions + +- `SecurityOrchardCoreBuilderExtensions`: Adds `BuilderExtensions` extensios. For example, the `ConfigureSecurityDefaults()` that provides some default security configuration for Orchard Core. diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Readme.md b/Lombiq.HelpfulLibraries.OrchardCore/Readme.md index 3acfdd9c..fc5334f0 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Readme.md +++ b/Lombiq.HelpfulLibraries.OrchardCore/Readme.md @@ -20,6 +20,7 @@ For general details about and on using the Helpful Libraries see the [root Readm - [Mvc](Docs/Mvc.md) - [Navigation](Docs/Navigation.md) - [ResourceManagement](Docs/ResourceManagement.md) +- [Security](Docs/Security.md) - [Settings](Docs/Settings.md) - [Shapes](Docs/Shapes.md) - [TagHelpers](Docs/TagHelpers.md) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index c5e4f8c1..76eb6bea 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -17,8 +17,7 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo services.Configure(options => options.Cookie.SecurePolicy = CookieSecurePolicy.Always)); /// - /// Provides some default security configuration for Orchard Core. Use it in conjunction with . + /// Provides some default security configuration for Orchard Core. /// /// /// From 0c308db72aa77282e88067a7508adf32a0441c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 14:43:52 +0100 Subject: [PATCH 28/86] Typo. --- Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md index 48b099ca..a165cf56 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md @@ -2,4 +2,4 @@ ## Extensions -- `SecurityOrchardCoreBuilderExtensions`: Adds `BuilderExtensions` extensios. For example, the `ConfigureSecurityDefaults()` that provides some default security configuration for Orchard Core. +- `SecurityOrchardCoreBuilderExtensions`: Adds `BuilderExtensions` extensions. For example, the `ConfigureSecurityDefaults()` that provides some default security configuration for Orchard Core. From 539ac6e5bf5d943ae955bf52944c8126e4fcf675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 14:49:10 +0100 Subject: [PATCH 29/86] Annoying SA1629 doesn't let me have a block of quote on its own --- .../Security/ApplicationBuilderExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index c2eb7511..e1864841 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -56,11 +56,11 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati /// Adds a middleware that sets the X-Content-Type-Options header to nosniff. /// /// - /// As per the documentation: "The Anti-MIME-Sniffing - /// header X-Content-Type-Options was not set to ’nosniff’. This allows older versions of Internet Explorer and - /// Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted - /// and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions - /// of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing." + /// "The Anti-MIME-Sniffing header X-Content-Type-Options was not set to ’nosniff’. This allows older versions of + /// Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response + /// body to be interpreted and displayed as a content type other than the declared content type. Current (early + /// 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing + /// MIME-sniffing." As written in the documentation. /// public static IApplicationBuilder UseNosniffContentTypeOptionsHeader(this IApplicationBuilder app) => app.Use(async (context, next) => From 4bf1cd24e56b60cdf989736359beb602fea3383f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 16:14:52 +0100 Subject: [PATCH 30/86] Some code cleanup. --- .../AntiClickjackingContentSecurityPolicyProvider.cs | 4 ++++ .../Security/ServiceCollectionExtensions.cs | 6 ------ .../Security/OrchardCoreBuilderExtensions.cs | 11 +++++++++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs index 7c9a0568..dece746b 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs @@ -7,6 +7,10 @@ namespace Lombiq.HelpfulLibraries.AspNetCore.Security; +/// +/// A content security policy directive provider that sets the directive to , to prevent clickjacking. +/// public class AntiClickjackingContentSecurityPolicyProvider : IContentSecurityPolicyProvider { public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs index 87c8a7b1..b1a648e4 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ServiceCollectionExtensions.cs @@ -14,12 +14,6 @@ public static IServiceCollection AddContentSecurityPolicyProvider(thi where TProvider : class, IContentSecurityPolicyProvider => services.AddScoped(); - /// - /// Registers . - /// - public static IServiceCollection AddAntiClickjackingContentSecurityPolicyProvider(this IServiceCollection services) => - services.AddContentSecurityPolicyProvider(); - /// /// Configures the session cookie to be always secure. With this configuration the token won't work in an HTTP /// environment so make sure that HTTPS redirection is enabled. diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 76eb6bea..192d4724 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -28,7 +28,12 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo /// /// /// - /// Call to make the anti-forgery token secure. + /// Make the session token's cookie always secure. + /// + /// + /// + /// + /// Make the anti-forgery token's cookie always secure. /// /// /// @@ -52,7 +57,9 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuilder builder) { builder.ApplicationServices.AddInlineStartup( - services => services.AddAntiClickjackingContentSecurityPolicyProvider(), + services => services + .AddContentSecurityPolicyProvider() + .ConfigureSessionCookieAlwaysSecure(), app => app .UseContentSecurityPolicyHeader(allowInline: true) .UseNosniffContentTypeOptionsHeader(), From f60e6430fbfda218e312e044bfc418f457093607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 16:16:10 +0100 Subject: [PATCH 31/86] Add AddContentSecurityPolicyProvider --- .../CdnContentSecurityPolicyProvider.cs | 48 +++++++++++++++++++ .../Security/OrchardCoreBuilderExtensions.cs | 7 +++ 2 files changed, 55 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs new file mode 100644 index 00000000..8e383477 --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; + +namespace Lombiq.HelpfulLibraries.AspNetCore.Security; + +/// +/// A content security policy directive provider that provides additional permitted host names for and . +/// +public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider +{ + /// + /// Gets or sets the URLs whose will be added to the directive. + /// + public static ICollection PermittedStyleSources { get; set; } = new[] + { + new Uri("https://fonts.googleapis.com/css"), + new Uri("https://cdn.jsdelivr.net/npm"), + }; + + /// + /// Gets or sets the URLs whose will be added to the directive. + /// + public static ICollection PermittedScriptSources { get; set; } = new[] + { + new Uri("https://cdn.jsdelivr.net/npm"), + }; + + public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) + { + if (PermittedStyleSources.Any()) + { + securityPolicies[StyleSrc] += " " + string.Join(' ', PermittedStyleSources.Select(uri => uri.Host)); + } + + if (PermittedScriptSources.Any()) + { + securityPolicies[ScriptSrc] += " " + string.Join(' ', PermittedScriptSources.Select(uri => uri.Host)); + } + + return ValueTask.CompletedTask; + } +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 192d4724..68ae7e60 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -28,6 +28,12 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo /// /// /// + /// Add to permit using script and style resources from + /// some common CDNs. + /// + /// + /// + /// /// Make the session token's cookie always secure. /// /// @@ -59,6 +65,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuild builder.ApplicationServices.AddInlineStartup( services => services .AddContentSecurityPolicyProvider() + .AddContentSecurityPolicyProvider() .ConfigureSessionCookieAlwaysSecure(), app => app .UseContentSecurityPolicyHeader(allowInline: true) From 7651b15f082072f56f198d8286ace2e53a79ec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 16:35:54 +0100 Subject: [PATCH 32/86] These may be amended during program setup. --- .../Security/CdnContentSecurityPolicyProvider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs index 8e383477..8e38b54e 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -14,6 +14,8 @@ namespace Lombiq.HelpfulLibraries.AspNetCore.Security; /// public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider { + // These may be amended during program setup. +#pragma warning disable CA2227 // CA2227: Change 'PermittedStyleSources' to be read-only by removing the property setter /// /// Gets or sets the URLs whose will be added to the directive. /// @@ -30,6 +32,7 @@ public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider { new Uri("https://cdn.jsdelivr.net/npm"), }; +#pragma warning restore CA2227 // CA2227: Change 'PermittedStyleSources' to be read-only by removing the property setter public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { From e9dd93174b445d809a544d9aa9ff999141e5bae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 16:50:26 +0100 Subject: [PATCH 33/86] Merge the anti-clickjacking provider into the main UseContentSecurityPolicyHeader. --- ...ickjackingContentSecurityPolicyProvider.cs | 22 ------------------- .../Security/ApplicationBuilderExtensions.cs | 6 ++++- .../Security/OrchardCoreBuilderExtensions.cs | 6 ----- 3 files changed, 5 insertions(+), 29 deletions(-) delete mode 100644 Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs deleted file mode 100644 index dece746b..00000000 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/AntiClickjackingContentSecurityPolicyProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System.Collections.Generic; -using System.Threading.Tasks; - -using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; -using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; - -namespace Lombiq.HelpfulLibraries.AspNetCore.Security; - -/// -/// A content security policy directive provider that sets the directive to , to prevent clickjacking. -/// -public class AntiClickjackingContentSecurityPolicyProvider : IContentSecurityPolicyProvider -{ - public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) - { - securityPolicies[FrameAncestors] = Self; - - return ValueTask.CompletedTask; - } -} diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index e1864841..a8b7cae3 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; - using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; @@ -31,6 +30,8 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati [ImgSrc] = $"{Self} {Data}", // Modern sites shouldn't need , , and elements. [ObjectSrc] = None, + // Necessary to prevent clickjacking (https://developer.mozilla.org/en-US/docs/Glossary/Clickjacking). + [FrameAncestors] = Self, }; // Orchard Core setup will fail without 'unsafe-inline'. Additionally, it's almost guaranteed that some page @@ -41,6 +42,9 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati securityPolicies[StyleSrc] = $"{Self} {UnsafeInline}"; } + // The thought behind this provider model is that if you need something else than the default, you should + // add a provider that only applies the additional directive on screens where it's actually needed. This way + // we maintain minimal permissions. If you need additional foreach (var provider in context.RequestServices.GetService>()) { await provider.UpdateAsync(securityPolicies, context); diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 68ae7e60..2fd5ebc9 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -23,11 +23,6 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo /// /// /// - /// Add to prevent clickjacking. - /// - /// - /// - /// /// Add to permit using script and style resources from /// some common CDNs. /// @@ -64,7 +59,6 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuild { builder.ApplicationServices.AddInlineStartup( services => services - .AddContentSecurityPolicyProvider() .AddContentSecurityPolicyProvider() .ConfigureSessionCookieAlwaysSecure(), app => app From 2627cb5f0e7560560dd6a04c57fb2f74acea2d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 17:15:44 +0100 Subject: [PATCH 34/86] Permit fonts.gstatic.com --- .../Security/CdnContentSecurityPolicyProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs index 8e38b54e..6abb1e32 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -22,6 +22,7 @@ public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider public static ICollection PermittedStyleSources { get; set; } = new[] { new Uri("https://fonts.googleapis.com/css"), + new Uri("https://fonts.gstatic.com/"), new Uri("https://cdn.jsdelivr.net/npm"), }; From d3b0e505008e054a57660c1189d53507da360316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 18:36:15 +0100 Subject: [PATCH 35/86] Update security documentation. --- Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md | 5 ++++- Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md index 52fb7b84..aeee3fb1 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md +++ b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md @@ -3,6 +3,9 @@ ## `Content-Security-Policy` - `ApplicationBuilderExtensions`: Contains the `AddContentSecurityPolicyHeader` extension method to add a middleware that provides the `Content-Security-Policy` header. +- `CdnContentSecurityPolicyProvider`: An optional policy provider that permits additional CDN host names for the `script-scr` and `style-src` directives. - `ContentSecurityPolicyDirectives`: The `Content-Security-Policy` directive names that are defined in the W3C [recommendation](https://www.w3.org/TR/CSP2/#directives) and some common values. - `IContentSecurityPolicyProvider`: Interface for services that update the dictionary that will be turned into the `Content-Security-Policy` header value. -- `AntiClickjackingContentSecurityPolicyProvider`: An optional policy provider that prevents clickjacking by including the `frame-ancestors 'self'` directive. +- `ServiceCollectionExtensions`: Extensions methods for `IServiceCollection`, e.g. `AddContentSecurityPolicyProvider()` is a shortcut to register `IContentSecurityPolicyProvider` in dependency injection. + +There is a similar section for security extensions related to Orchard Core [here](../../Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md). diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md index a165cf56..34b6438e 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md @@ -3,3 +3,5 @@ ## Extensions - `SecurityOrchardCoreBuilderExtensions`: Adds `BuilderExtensions` extensions. For example, the `ConfigureSecurityDefaults()` that provides some default security configuration for Orchard Core. + +There is a similar section for security extensions related to ASP.NET Core [here](../../Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md). \ No newline at end of file From 945f7c2f7b657ceb0bba43755b2d9ec9c87570d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 19:50:18 +0100 Subject: [PATCH 36/86] Prevent adding the "Content-Security-Policy" header twice. --- .../Security/ApplicationBuilderExtensions.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index a8b7cae3..5710e7c2 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -17,6 +17,14 @@ public static class ApplicationBuilderExtensions public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicationBuilder app, bool allowInline) => app.Use(async (context, next) => { + const string key = "Content-Security-Policy"; + + if (context.Response.Headers.ContainsKey(key)) + { + await next(); + return; + } + var securityPolicies = new Dictionary(StringComparer.OrdinalIgnoreCase) { // Default values enforcing a same origin policy for all resources. @@ -51,7 +59,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati } var policy = string.Join("; ", securityPolicies.Select((key, value) => $"{key} {value}")); - context.Response.Headers.Add("Content-Security-Policy", policy); + context.Response.Headers.Add(key, policy); await next(); }); From fcf89591da0135d782ebacbe6808139cc40d758e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 19:51:05 +0100 Subject: [PATCH 37/86] Skip security middlewares during setup. --- ...Lombiq.HelpfulLibraries.OrchardCore.csproj | 1 + .../Security/OrchardCoreBuilderExtensions.cs | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj b/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj index 1441bef3..b313eb54 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj +++ b/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj @@ -44,6 +44,7 @@ + diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 2fd5ebc9..d7954cae 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -3,6 +3,9 @@ using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using OrchardCore.Environment.Shell; +using OrchardCore.Environment.Shell.Models; +using System.Linq; namespace Microsoft.Extensions.DependencyInjection; @@ -61,9 +64,19 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuild services => services .AddContentSecurityPolicyProvider() .ConfigureSessionCookieAlwaysSecure(), - app => app - .UseContentSecurityPolicyHeader(allowInline: true) - .UseNosniffContentTypeOptionsHeader(), + (app, _, serviceProvider) => + { + // Don't add any middlewares if the site is in setup mode. + var shellSettings = serviceProvider + .GetRequiredService() + .GetAllSettings() + .FirstOrDefault(settings => settings.Name == "Default"); + if (shellSettings?.State == TenantState.Uninitialized) return; + + app + .UseContentSecurityPolicyHeader(allowInline: true) + .UseNosniffContentTypeOptionsHeader(); + }, order: 99); return builder .ConfigureAntiForgeryAlwaysSecure() From 9647be5188471ff2c72817ced27e9c01432acfc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 21:29:42 +0100 Subject: [PATCH 38/86] Control inline scripts and styles separately. --- .../Security/ApplicationBuilderExtensions.cs | 23 +++++++++++-------- .../Security/OrchardCoreBuilderExtensions.cs | 7 ++++-- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 5710e7c2..a0052c25 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -13,8 +13,18 @@ public static class ApplicationBuilderExtensions /// Adds a middleware that supplies the Content-Security-Policy header. It may be further expanded by /// registering services that implement . /// - /// If then inline scripts and styles are permitted. - public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicationBuilder app, bool allowInline) => + /// + /// If then inline scripts are permitted. When using Orchard Core a lot of front end shapes + /// use inline script blocks making this a required setting. + /// + /// + /// If then inline styles are permitted. Note that even if your site has no embedded style + /// blocks and no style attributes, some Javascript libraries may still create some from code. + /// + public static IApplicationBuilder UseContentSecurityPolicyHeader( + this IApplicationBuilder app, + bool allowInlineScript, + bool allowInlineStyle) => app.Use(async (context, next) => { const string key = "Content-Security-Policy"; @@ -42,13 +52,8 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader(this IApplicati [FrameAncestors] = Self, }; - // Orchard Core setup will fail without 'unsafe-inline'. Additionally, it's almost guaranteed that some page - // will contain non-precompiled Vue.js code from built-in OC features and that requires 'unsafe-eval'. - if (allowInline) - { - securityPolicies[ScriptSrc] = $"{Self} {UnsafeInline} {UnsafeEval}"; - securityPolicies[StyleSrc] = $"{Self} {UnsafeInline}"; - } + if (allowInlineScript) securityPolicies[ScriptSrc] = $"{Self} {UnsafeInline}"; + if (allowInlineStyle) securityPolicies[StyleSrc] = $"{Self} {UnsafeInline}"; // The thought behind this provider model is that if you need something else than the default, you should // add a provider that only applies the additional directive on screens where it's actually needed. This way diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index d7954cae..6dcfb82a 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -58,7 +58,10 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo /// /// /// - public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuilder builder) + public static OrchardCoreBuilder ConfigureSecurityDefaults( + this OrchardCoreBuilder builder, + bool allowInlineScript = true, + bool allowInlineStyle = false) { builder.ApplicationServices.AddInlineStartup( services => services @@ -74,7 +77,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults(this OrchardCoreBuild if (shellSettings?.State == TenantState.Uninitialized) return; app - .UseContentSecurityPolicyHeader(allowInline: true) + .UseContentSecurityPolicyHeader(allowInlineScript, allowInlineStyle) .UseNosniffContentTypeOptionsHeader(); }, order: 99); From a2a865186e148f9518bbfde17c440f5db49d870f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 21:52:35 +0100 Subject: [PATCH 39/86] Fix formatting. --- Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md | 2 +- Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md index aeee3fb1..4a94902e 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md +++ b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md @@ -8,4 +8,4 @@ - `IContentSecurityPolicyProvider`: Interface for services that update the dictionary that will be turned into the `Content-Security-Policy` header value. - `ServiceCollectionExtensions`: Extensions methods for `IServiceCollection`, e.g. `AddContentSecurityPolicyProvider()` is a shortcut to register `IContentSecurityPolicyProvider` in dependency injection. -There is a similar section for security extensions related to Orchard Core [here](../../Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md). +There is a similar section for security extensions related to Orchard Core [here](../../Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md). diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md index 34b6438e..53658eba 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md @@ -4,4 +4,4 @@ - `SecurityOrchardCoreBuilderExtensions`: Adds `BuilderExtensions` extensions. For example, the `ConfigureSecurityDefaults()` that provides some default security configuration for Orchard Core. -There is a similar section for security extensions related to ASP.NET Core [here](../../Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md). \ No newline at end of file +There is a similar section for security extensions related to ASP.NET Core [here](../../Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md). From 7f97b99258230c494469a032787177c472da1b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 29 Dec 2023 22:09:51 +0100 Subject: [PATCH 40/86] Adjust doc. --- .../Security/ApplicationBuilderExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index a0052c25..0c2c4745 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -15,7 +15,8 @@ public static class ApplicationBuilderExtensions /// /// /// If then inline scripts are permitted. When using Orchard Core a lot of front end shapes - /// use inline script blocks making this a required setting. + /// use inline script blocks without a nonce (see https://github.com/OrchardCMS/OrchardCore/issues/13389) making + /// this a required setting. /// /// /// If then inline styles are permitted. Note that even if your site has no embedded style From 24918396d03da7a1388473bc6ecae3808c47993c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Thu, 4 Jan 2024 22:12:24 +0100 Subject: [PATCH 41/86] Update the header at the end of the pipeline. --- .../Security/ApplicationBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 0c2c4745..35a2804c 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -56,6 +56,8 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( if (allowInlineScript) securityPolicies[ScriptSrc] = $"{Self} {UnsafeInline}"; if (allowInlineStyle) securityPolicies[StyleSrc] = $"{Self} {UnsafeInline}"; + await next(); + // The thought behind this provider model is that if you need something else than the default, you should // add a provider that only applies the additional directive on screens where it's actually needed. This way // we maintain minimal permissions. If you need additional @@ -66,8 +68,6 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( var policy = string.Join("; ", securityPolicies.Select((key, value) => $"{key} {value}")); context.Response.Headers.Add(key, policy); - - await next(); }); /// From 57f8ede55440dc596994fc9baa7f74b28b522ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 5 Jan 2024 01:28:06 +0100 Subject: [PATCH 42/86] Add VueContentSecurityPolicyProvider --- .../Security/OrchardCoreBuilderExtensions.cs | 1 + .../VueContentSecurityPolicyProvider.cs | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 6dcfb82a..84c27ce4 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -66,6 +66,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( builder.ApplicationServices.AddInlineStartup( services => services .AddContentSecurityPolicyProvider() + .AddContentSecurityPolicyProvider() .ConfigureSessionCookieAlwaysSecure(), (app, _, serviceProvider) => { diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs new file mode 100644 index 00000000..4a26f711 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs @@ -0,0 +1,33 @@ +using Lombiq.HelpfulLibraries.AspNetCore.Security; +using Microsoft.AspNetCore.Http; +using OrchardCore.ResourceManagement; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Enable the value for the directive. This is necessary to evaluate +/// dynamic (not precompiled) templates. These are extensively used in stock Orchard Core. Also in many third party +/// modules where the DOM HTML template may contain Razor generated content. +/// +public class VueContentSecurityPolicyProvider : IContentSecurityPolicyProvider +{ + private readonly IResourceManager _resourceManager; + + public VueContentSecurityPolicyProvider(IResourceManager resourceManager) => + _resourceManager = resourceManager; + + public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) + { + if (_resourceManager.GetRequiredResources("script").Any(script => script.Resource.Name == "vuejs")) + { + securityPolicies[ScriptSrc] += ' ' + UnsafeEval; + } + + return ValueTask.CompletedTask; + } +} From 56245a4e244b49d98c076bdbab1fb5afdd2271b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 5 Jan 2024 18:41:55 +0100 Subject: [PATCH 43/86] Move it into the OnStarting event. --- .../Security/ApplicationBuilderExtensions.cs | 32 ++++++++++++------- .../Security/OrchardCoreBuilderExtensions.cs | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 35a2804c..65e8defa 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; +using System.Threading.Tasks; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; @@ -22,10 +23,16 @@ public static class ApplicationBuilderExtensions /// If then inline styles are permitted. Note that even if your site has no embedded style /// blocks and no style attributes, some Javascript libraries may still create some from code. /// + /// + /// If then the providers are evaluated on the + /// return end of the pipeline. This incurs some cost because the response body has to be cached, otherwise the + /// headers won't be editable by that point. + /// public static IApplicationBuilder UseContentSecurityPolicyHeader( this IApplicationBuilder app, bool allowInlineScript, - bool allowInlineStyle) => + bool allowInlineStyle, + bool isDeferred) => app.Use(async (context, next) => { const string key = "Content-Security-Policy"; @@ -56,18 +63,21 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( if (allowInlineScript) securityPolicies[ScriptSrc] = $"{Self} {UnsafeInline}"; if (allowInlineStyle) securityPolicies[StyleSrc] = $"{Self} {UnsafeInline}"; - await next(); - - // The thought behind this provider model is that if you need something else than the default, you should - // add a provider that only applies the additional directive on screens where it's actually needed. This way - // we maintain minimal permissions. If you need additional - foreach (var provider in context.RequestServices.GetService>()) + context.Response.OnStarting(async () => { - await provider.UpdateAsync(securityPolicies, context); - } + // The thought behind this provider model is that if you need something else than the default, you should + // add a provider that only applies the additional directive on screens where it's actually needed. This way + // we maintain minimal permissions. If you need additional + foreach (var provider in context.RequestServices.GetService>()) + { + await provider.UpdateAsync(securityPolicies, context); + } + + var policy = string.Join("; ", securityPolicies.Select((key, value) => $"{key} {value}")); + context.Response.Headers.Add(key, policy); + }); - var policy = string.Join("; ", securityPolicies.Select((key, value) => $"{key} {value}")); - context.Response.Headers.Add(key, policy); + await next(); }); /// diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 84c27ce4..1e77b349 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -78,7 +78,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( if (shellSettings?.State == TenantState.Uninitialized) return; app - .UseContentSecurityPolicyHeader(allowInlineScript, allowInlineStyle) + .UseContentSecurityPolicyHeader(allowInlineScript, allowInlineStyle, false) .UseNosniffContentTypeOptionsHeader(); }, order: 99); From 7c1c76ad3d3d72a5bf9762eb0ce07762f210b998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 5 Jan 2024 23:29:27 +0100 Subject: [PATCH 44/86] No need for isDeferred after all. --- .../Security/ApplicationBuilderExtensions.cs | 8 +------- .../Security/OrchardCoreBuilderExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 65e8defa..9c3752f0 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -23,16 +23,10 @@ public static class ApplicationBuilderExtensions /// If then inline styles are permitted. Note that even if your site has no embedded style /// blocks and no style attributes, some Javascript libraries may still create some from code. /// - /// - /// If then the providers are evaluated on the - /// return end of the pipeline. This incurs some cost because the response body has to be cached, otherwise the - /// headers won't be editable by that point. - /// public static IApplicationBuilder UseContentSecurityPolicyHeader( this IApplicationBuilder app, bool allowInlineScript, - bool allowInlineStyle, - bool isDeferred) => + bool allowInlineStyle) => app.Use(async (context, next) => { const string key = "Content-Security-Policy"; diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 1e77b349..84c27ce4 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -78,7 +78,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( if (shellSettings?.State == TenantState.Uninitialized) return; app - .UseContentSecurityPolicyHeader(allowInlineScript, allowInlineStyle, false) + .UseContentSecurityPolicyHeader(allowInlineScript, allowInlineStyle) .UseNosniffContentTypeOptionsHeader(); }, order: 99); From 2ae42243dd80a356378f260cde06d499ac1b185f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 5 Jan 2024 23:56:56 +0100 Subject: [PATCH 45/86] unusing --- .../Security/ApplicationBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 9c3752f0..24a6efaf 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; -using System.Threading.Tasks; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; From a20552870b3a50cebf293a2586c2274da0825fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Jan 2024 19:34:36 +0100 Subject: [PATCH 46/86] Update Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Security/CdnContentSecurityPolicyProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs index 6abb1e32..7194649e 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -19,7 +19,7 @@ public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider /// /// Gets or sets the URLs whose will be added to the directive. /// - public static ICollection PermittedStyleSources { get; set; } = new[] + public static IReadOnlyCollection PermittedStyleSources { get; } = new[] { new Uri("https://fonts.googleapis.com/css"), new Uri("https://fonts.gstatic.com/"), @@ -29,7 +29,7 @@ public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider /// /// Gets or sets the URLs whose will be added to the directive. /// - public static ICollection PermittedScriptSources { get; set; } = new[] + public static IReadOnlyCollection PermittedScriptSources { get; } = new[] { new Uri("https://cdn.jsdelivr.net/npm"), }; From 2e5b7f5b18d9e6178a936438bf063baad79ce173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Jan 2024 19:35:03 +0100 Subject: [PATCH 47/86] Use GetServices. --- .../Security/ApplicationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 24a6efaf..8d9ca26b 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -61,7 +61,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( // The thought behind this provider model is that if you need something else than the default, you should // add a provider that only applies the additional directive on screens where it's actually needed. This way // we maintain minimal permissions. If you need additional - foreach (var provider in context.RequestServices.GetService>()) + foreach (var provider in context.RequestServices.GetServices()) { await provider.UpdateAsync(securityPolicies, context); } From 0d37c7940d0fd0d241113311207afeeea04416f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Jan 2024 19:38:45 +0100 Subject: [PATCH 48/86] comment --- .../Security/OrchardCoreBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 84c27ce4..ac915341 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -81,7 +81,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( .UseContentSecurityPolicyHeader(allowInlineScript, allowInlineStyle) .UseNosniffContentTypeOptionsHeader(); }, - order: 99); + order: 99); // Makes this service load fairly late. This should make the setup detection more accurate. return builder .ConfigureAntiForgeryAlwaysSecure() .AddTenantFeatures("OrchardCore.Diagnostics"); From 5cf2a8d957835304cce649903a7d92fe861b8ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Jan 2024 19:42:51 +0100 Subject: [PATCH 49/86] Update Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Security/CdnContentSecurityPolicyProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs index 7194649e..5e99f353 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -39,12 +39,12 @@ public ValueTask UpdateAsync(IDictionary securityPolicies, HttpC { if (PermittedStyleSources.Any()) { - securityPolicies[StyleSrc] += " " + string.Join(' ', PermittedStyleSources.Select(uri => uri.Host)); + securityPolicies[StyleSrc] = string.Join(' ', securityPolicies[StyleSrc].Split(' ').Union(PermittedStyleSources.Select(uri => uri.Host)).Distinct()); } if (PermittedScriptSources.Any()) { - securityPolicies[ScriptSrc] += " " + string.Join(' ', PermittedScriptSources.Select(uri => uri.Host)); + securityPolicies[ScriptSrc] = string.Join(' ', securityPolicies[ScriptSrc].Split(' ').Union(PermittedScriptSources.Select(uri => uri.Host)).Distinct()); } return ValueTask.CompletedTask; From 45d3129a814eec9454cc19dd007e015e318c5b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Jan 2024 20:46:08 +0100 Subject: [PATCH 50/86] Code cleanup in CdnContentSecurityPolicyProvider. --- .../Security/CdnContentSecurityPolicyProvider.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs index 5e99f353..d40d5237 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -17,7 +17,7 @@ public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider // These may be amended during program setup. #pragma warning disable CA2227 // CA2227: Change 'PermittedStyleSources' to be read-only by removing the property setter /// - /// Gets or sets the URLs whose will be added to the directive. + /// Gets the URLs whose will be added to the directive. /// public static IReadOnlyCollection PermittedStyleSources { get; } = new[] { @@ -27,7 +27,7 @@ public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider }; /// - /// Gets or sets the URLs whose will be added to the directive. + /// Gets the URLs whose will be added to the directive. /// public static IReadOnlyCollection PermittedScriptSources { get; } = new[] { @@ -39,14 +39,20 @@ public ValueTask UpdateAsync(IDictionary securityPolicies, HttpC { if (PermittedStyleSources.Any()) { - securityPolicies[StyleSrc] = string.Join(' ', securityPolicies[StyleSrc].Split(' ').Union(PermittedStyleSources.Select(uri => uri.Host)).Distinct()); + securityPolicies[StyleSrc] = MergeValues(securityPolicies[StyleSrc], PermittedStyleSources); } if (PermittedScriptSources.Any()) { - securityPolicies[ScriptSrc] = string.Join(' ', securityPolicies[ScriptSrc].Split(' ').Union(PermittedScriptSources.Select(uri => uri.Host)).Distinct()); + securityPolicies[ScriptSrc] = MergeValues(securityPolicies[ScriptSrc], PermittedScriptSources); } return ValueTask.CompletedTask; } + + private static string MergeValues(string directiveValue, IEnumerable sources) => + string.Join(' ', directiveValue + .Split(' ') + .Union(sources.Select(uri => uri.Host)) + .Distinct()); } From 02ff2f5042ed6bae916cbc08372e7c7e76fb9cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Jan 2024 21:00:26 +0100 Subject: [PATCH 51/86] Add font-src to CdnContentSecurityPolicyProvider. --- .../CdnContentSecurityPolicyProvider.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs index d40d5237..cc24ad5d 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -14,8 +14,6 @@ namespace Lombiq.HelpfulLibraries.AspNetCore.Security; /// public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider { - // These may be amended during program setup. -#pragma warning disable CA2227 // CA2227: Change 'PermittedStyleSources' to be read-only by removing the property setter /// /// Gets the URLs whose will be added to the directive. /// @@ -33,7 +31,15 @@ public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider { new Uri("https://cdn.jsdelivr.net/npm"), }; -#pragma warning restore CA2227 // CA2227: Change 'PermittedStyleSources' to be read-only by removing the property setter + + /// + /// Gets the URLs whose will be added to the directive. + /// + public static IReadOnlyCollection PermittedFontSources { get; } = new[] + { + new Uri("https://fonts.googleapis.com/"), + new Uri("https://fonts.gstatic.com/"), + }; public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { @@ -47,6 +53,11 @@ public ValueTask UpdateAsync(IDictionary securityPolicies, HttpC securityPolicies[ScriptSrc] = MergeValues(securityPolicies[ScriptSrc], PermittedScriptSources); } + if (PermittedFontSources.Any()) + { + securityPolicies[FontSrc] = MergeValues(securityPolicies[FontSrc], PermittedFontSources); + } + return ValueTask.CompletedTask; } From 3cda6bce34d286f5df07e818d68f11ae54e8db8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Jan 2024 21:14:51 +0100 Subject: [PATCH 52/86] Fall back if font-src is not defined --- .../Security/CdnContentSecurityPolicyProvider.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs index cc24ad5d..edda17c3 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -45,25 +45,29 @@ public ValueTask UpdateAsync(IDictionary securityPolicies, HttpC { if (PermittedStyleSources.Any()) { - securityPolicies[StyleSrc] = MergeValues(securityPolicies[StyleSrc], PermittedStyleSources); + MergeValues(securityPolicies, StyleSrc, PermittedStyleSources); } if (PermittedScriptSources.Any()) { - securityPolicies[ScriptSrc] = MergeValues(securityPolicies[ScriptSrc], PermittedScriptSources); + MergeValues(securityPolicies, ScriptSrc, PermittedScriptSources); } if (PermittedFontSources.Any()) { - securityPolicies[FontSrc] = MergeValues(securityPolicies[FontSrc], PermittedFontSources); + MergeValues(securityPolicies, FontSrc, PermittedFontSources); } return ValueTask.CompletedTask; } - private static string MergeValues(string directiveValue, IEnumerable sources) => - string.Join(' ', directiveValue + private static void MergeValues(IDictionary policies, string key, IEnumerable sources) + { + var directiveValue = policies.GetMaybe(key) ?? policies.GetMaybe(DefaultSrc) ?? string.Empty; + + policies[key] = string.Join(' ', directiveValue .Split(' ') .Union(sources.Select(uri => uri.Host)) .Distinct()); + } } From 5a8dca8f4a12c565ae45eb5a06c71e4315cf1e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Jan 2024 21:33:46 +0100 Subject: [PATCH 53/86] Use already existing constant for default tenant name. --- .../Security/OrchardCoreBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index ac915341..db332747 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -74,7 +74,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( var shellSettings = serviceProvider .GetRequiredService() .GetAllSettings() - .FirstOrDefault(settings => settings.Name == "Default"); + .FirstOrDefault(settings => settings.Name == ShellSettings.DefaultShellName); if (shellSettings?.State == TenantState.Uninitialized) return; app From e5ed11d815138601d5b101531527644c26a27d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Jan 2024 22:23:44 +0100 Subject: [PATCH 54/86] Exclude CSP for error page and non-HTML. --- .../Security/ApplicationBuilderExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 8d9ca26b..66e3842a 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -58,6 +58,9 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( context.Response.OnStarting(async () => { + // No need to do content security policy on the "Not Found" page or on non-HTML responses. + if (context.Response.StatusCode == 404 || !context.Response.ContentType.ContainsOrdinalIgnoreCase("text/html")) return; + // The thought behind this provider model is that if you need something else than the default, you should // add a provider that only applies the additional directive on screens where it's actually needed. This way // we maintain minimal permissions. If you need additional From a0793b7c4b66c7a0e58eda0ec471b7f2f24ccd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sun, 7 Jan 2024 10:07:10 +0100 Subject: [PATCH 55/86] Fix content type check. --- .../Security/ApplicationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 66e3842a..24b6b731 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -59,7 +59,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( context.Response.OnStarting(async () => { // No need to do content security policy on the "Not Found" page or on non-HTML responses. - if (context.Response.StatusCode == 404 || !context.Response.ContentType.ContainsOrdinalIgnoreCase("text/html")) return; + if (context.Response.StatusCode == 404 || !context.Response.ContentType?.ContainsOrdinalIgnoreCase("text/html") == true) return; // The thought behind this provider model is that if you need something else than the default, you should // add a provider that only applies the additional directive on screens where it's actually needed. This way From 4441bf527ff745386a095579bf995e0871138f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sun, 7 Jan 2024 10:13:35 +0100 Subject: [PATCH 56/86] Need CSP for 404 after all. --- .../Security/ApplicationBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 24b6b731..301b224d 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -58,8 +58,8 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( context.Response.OnStarting(async () => { - // No need to do content security policy on the "Not Found" page or on non-HTML responses. - if (context.Response.StatusCode == 404 || !context.Response.ContentType?.ContainsOrdinalIgnoreCase("text/html") == true) return; + // No need to do content security policy on non-HTML responses. + if (!context.Response.ContentType?.ContainsOrdinalIgnoreCase("text/html") == true) return; // The thought behind this provider model is that if you need something else than the default, you should // add a provider that only applies the additional directive on screens where it's actually needed. This way From 32b529e0c9d6c1cb6f212740c8fa0366b9ed99ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sun, 7 Jan 2024 10:18:42 +0100 Subject: [PATCH 57/86] Remove stray exclamation point. --- .../Security/ApplicationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 301b224d..aa802fca 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -59,7 +59,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( context.Response.OnStarting(async () => { // No need to do content security policy on non-HTML responses. - if (!context.Response.ContentType?.ContainsOrdinalIgnoreCase("text/html") == true) return; + if (context.Response.ContentType?.ContainsOrdinalIgnoreCase("text/html") == true) return; // The thought behind this provider model is that if you need something else than the default, you should // add a provider that only applies the additional directive on screens where it's actually needed. This way From 42ffcb01b1b2c6fa4c986526e99eb1f8f9c6976a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sun, 7 Jan 2024 10:52:41 +0100 Subject: [PATCH 58/86] The other way around. --- .../Security/ApplicationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index aa802fca..265d20b4 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -59,7 +59,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( context.Response.OnStarting(async () => { // No need to do content security policy on non-HTML responses. - if (context.Response.ContentType?.ContainsOrdinalIgnoreCase("text/html") == true) return; + if (context.Response.ContentType?.ContainsOrdinalIgnoreCase("text/html") != true) return; // The thought behind this provider model is that if you need something else than the default, you should // add a provider that only applies the additional directive on screens where it's actually needed. This way From 204cc49241eb9d910b6d53f8176334a717795bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 8 Jan 2024 09:41:34 +0100 Subject: [PATCH 59/86] Use ConcurrentBag instead of ReadOnlyCollection so these are editable. --- .../Security/CdnContentSecurityPolicyProvider.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs index edda17c3..8236bbb8 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -17,29 +18,29 @@ public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider /// /// Gets the URLs whose will be added to the directive. /// - public static IReadOnlyCollection PermittedStyleSources { get; } = new[] + public static ConcurrentBag PermittedStyleSources { get; } = new(new[] { new Uri("https://fonts.googleapis.com/css"), new Uri("https://fonts.gstatic.com/"), new Uri("https://cdn.jsdelivr.net/npm"), - }; + }); /// /// Gets the URLs whose will be added to the directive. /// - public static IReadOnlyCollection PermittedScriptSources { get; } = new[] + public static ConcurrentBag PermittedScriptSources { get; } = new(new[] { new Uri("https://cdn.jsdelivr.net/npm"), - }; + }); /// /// Gets the URLs whose will be added to the directive. /// - public static IReadOnlyCollection PermittedFontSources { get; } = new[] + public static ConcurrentBag PermittedFontSources { get; } = new(new[] { new Uri("https://fonts.googleapis.com/"), new Uri("https://fonts.gstatic.com/"), - }; + }); public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { From a26713a0e0a3908f5f40e99a1bf68535feb52826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 8 Jan 2024 09:43:05 +0100 Subject: [PATCH 60/86] Update connect-src with all permitted sources. --- .../Security/CdnContentSecurityPolicyProvider.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs index 8236bbb8..0f38bebe 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/CdnContentSecurityPolicyProvider.cs @@ -44,21 +44,32 @@ public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { + var any = false; + if (PermittedStyleSources.Any()) { + any = true; MergeValues(securityPolicies, StyleSrc, PermittedStyleSources); } if (PermittedScriptSources.Any()) { + any = true; MergeValues(securityPolicies, ScriptSrc, PermittedScriptSources); } if (PermittedFontSources.Any()) { + any = true; MergeValues(securityPolicies, FontSrc, PermittedFontSources); } + if (any) + { + var allPermittedSources = PermittedStyleSources.Concat(PermittedScriptSources).Concat(PermittedFontSources); + MergeValues(securityPolicies, ConnectSrc, allPermittedSources); + } + return ValueTask.CompletedTask; } From 0b2c7d77fa58586002ff1a505bba84fbc97c739f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Jan 2024 01:56:13 +0100 Subject: [PATCH 61/86] Add UseStrictAndSecureCookies --- .../Security/ApplicationBuilderExtensions.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 265d20b4..99cc4674 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -1,7 +1,10 @@ using Lombiq.HelpfulLibraries.AspNetCore.Security; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; using System; using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; @@ -98,4 +101,51 @@ public static IApplicationBuilder UseNosniffContentTypeOptionsHeader(this IAppli await next(); }); + + /// + /// Adds a middleware that checks all Set-Cookie headers and replaces any with a version containing + /// Secure and SameSite=Strict modifiers if they were missing. + /// cref="Cookie.Secure"/> and + /// + public static IApplicationBuilder UseStrictAndSecureCookies(this IApplicationBuilder app) => + app.Use((context, next) => + { + const string setCookieHeader = "Set-Cookie"; + context.Response.OnStarting(() => + { + var setCookie = context.Response.Headers[setCookieHeader]; + if (!setCookie.Any()) return Task.CompletedTask; + + var newCookies = new List(capacity: setCookie.Count); + var changed = false; + + foreach (var cookie in setCookie.WhereNot(string.IsNullOrWhiteSpace)) + { + var newCookie = cookie; + + if (!newCookie.ContainsOrdinalIgnoreCase("SameSite")) + { + newCookie += "; SameSite=Strict"; + changed = true; + } + + if (!cookie.ContainsOrdinalIgnoreCase("Secure")) + { + newCookie += "; Secure"; + changed = true; + } + + newCookies.Add(newCookie); + } + + if (changed) + { + context.Response.Headers[setCookieHeader] = new StringValues(newCookies.ToArray()); + } + + return Task.CompletedTask; + }); + + return next(); + }); } From fc1a38e4b5e034974b741bb8512304b1bb9b62fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Jan 2024 02:03:07 +0100 Subject: [PATCH 62/86] Fix errors. --- .../Security/ApplicationBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 99cc4674..82d270ef 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Primitives; using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Threading.Tasks; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; @@ -105,7 +106,6 @@ public static IApplicationBuilder UseNosniffContentTypeOptionsHeader(this IAppli /// /// Adds a middleware that checks all Set-Cookie headers and replaces any with a version containing /// Secure and SameSite=Strict modifiers if they were missing. - /// cref="Cookie.Secure"/> and /// public static IApplicationBuilder UseStrictAndSecureCookies(this IApplicationBuilder app) => app.Use((context, next) => @@ -114,7 +114,7 @@ public static IApplicationBuilder UseStrictAndSecureCookies(this IApplicationBui context.Response.OnStarting(() => { var setCookie = context.Response.Headers[setCookieHeader]; - if (!setCookie.Any()) return Task.CompletedTask; + if (!Enumerable.Any()) return Task.CompletedTask; var newCookies = new List(capacity: setCookie.Count); var changed = false; From fb99d1e6a6b8e08e32938b7a5db45946d91ce106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Jan 2024 11:56:56 +0100 Subject: [PATCH 63/86] Fix analyzer violations. --- .../Security/ApplicationBuilderExtensions.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 82d270ef..64a485fd 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading.Tasks; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; @@ -73,7 +72,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( await provider.UpdateAsync(securityPolicies, context); } - var policy = string.Join("; ", securityPolicies.Select((key, value) => $"{key} {value}")); + var policy = string.Join("; ", EnumerableExtensions.Select(securityPolicies, (key, value) => $"{key} {value}")); context.Response.Headers.Add(key, policy); }); @@ -107,14 +106,24 @@ public static IApplicationBuilder UseNosniffContentTypeOptionsHeader(this IAppli /// Adds a middleware that checks all Set-Cookie headers and replaces any with a version containing /// Secure and SameSite=Strict modifiers if they were missing. /// - public static IApplicationBuilder UseStrictAndSecureCookies(this IApplicationBuilder app) => - app.Use((context, next) => + public static IApplicationBuilder UseStrictAndSecureCookies(this IApplicationBuilder app) + { + static void UpdateIfMissing(ref string cookie, ref bool changed, string test, string append) + { + if (!cookie.ContainsOrdinalIgnoreCase(test)) + { + cookie += append; + changed = true; + } + } + + return app.Use((context, next) => { const string setCookieHeader = "Set-Cookie"; context.Response.OnStarting(() => { var setCookie = context.Response.Headers[setCookieHeader]; - if (!Enumerable.Any()) return Task.CompletedTask; + if (!setCookie.Any()) return Task.CompletedTask; var newCookies = new List(capacity: setCookie.Count); var changed = false; @@ -123,17 +132,8 @@ public static IApplicationBuilder UseStrictAndSecureCookies(this IApplicationBui { var newCookie = cookie; - if (!newCookie.ContainsOrdinalIgnoreCase("SameSite")) - { - newCookie += "; SameSite=Strict"; - changed = true; - } - - if (!cookie.ContainsOrdinalIgnoreCase("Secure")) - { - newCookie += "; Secure"; - changed = true; - } + UpdateIfMissing(ref newCookie, ref changed, "SameSite", "; SameSite=Strict"); + UpdateIfMissing(ref newCookie, ref changed, "Secure", "; Secure"); newCookies.Add(newCookie); } @@ -148,4 +148,5 @@ public static IApplicationBuilder UseStrictAndSecureCookies(this IApplicationBui return next(); }); + } } From 889cb2288e000aa48548f9a2ad1756555c19cfaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Jan 2024 12:46:47 +0100 Subject: [PATCH 64/86] Add UseStrictAndSecureCookies to the security defaults. --- .../Security/OrchardCoreBuilderExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index db332747..ed433b97 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -79,7 +79,8 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( app .UseContentSecurityPolicyHeader(allowInlineScript, allowInlineStyle) - .UseNosniffContentTypeOptionsHeader(); + .UseNosniffContentTypeOptionsHeader() + .UseStrictAndSecureCookies(); }, order: 99); // Makes this service load fairly late. This should make the setup detection more accurate. return builder From 6a373ce2bf85c84de271858bd8bd9c5137d3f009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Jan 2024 18:55:30 +0100 Subject: [PATCH 65/86] Don't insert duplicate CSP header. --- .../Security/ApplicationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 265d20b4..9892dbe2 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -70,7 +70,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( } var policy = string.Join("; ", securityPolicies.Select((key, value) => $"{key} {value}")); - context.Response.Headers.Add(key, policy); + context.Response.Headers[key] = policy; }); await next(); From 49940d6b442eb49b51cb7f6302b7cd505ea66bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Jan 2024 19:00:58 +0100 Subject: [PATCH 66/86] Code cleanup. --- .../Security/ApplicationBuilderExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 9ff5a07b..45c9bb29 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Mime; using System.Threading.Tasks; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; @@ -62,7 +63,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( context.Response.OnStarting(async () => { // No need to do content security policy on non-HTML responses. - if (context.Response.ContentType?.ContainsOrdinalIgnoreCase("text/html") != true) return; + if (context.Response.ContentType?.ContainsOrdinalIgnoreCase(MediaTypeNames.Text.Html) != true) return; // The thought behind this provider model is that if you need something else than the default, you should // add a provider that only applies the additional directive on screens where it's actually needed. This way @@ -72,7 +73,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( await provider.UpdateAsync(securityPolicies, context); } - var policy = string.Join("; ", EnumerableExtensions.Select(securityPolicies, (key, value) => $"{key} {value}")); + var policy = string.Join("; ", securityPolicies.Select(pair => $"{pair.Key} {pair.Value}")); context.Response.Headers[key] = policy; }); From 8c8a4733d687e47d87db162fec5e86d42f7dcf46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Jan 2024 19:41:29 +0100 Subject: [PATCH 67/86] Additional remarks about HTTPS. --- .../Security/ApplicationBuilderExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 45c9bb29..547370ed 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -107,6 +107,10 @@ public static IApplicationBuilder UseNosniffContentTypeOptionsHeader(this IAppli /// Adds a middleware that checks all Set-Cookie headers and replaces any with a version containing /// Secure and SameSite=Strict modifiers if they were missing. /// + /// + /// With this all cookies will only work in a secure context, so you should have some way to automatically redirect + /// any HTTP request to HTTPS. + /// public static IApplicationBuilder UseStrictAndSecureCookies(this IApplicationBuilder app) { static void UpdateIfMissing(ref string cookie, ref bool changed, string test, string append) From 2ee9107bf48364ccc48a5198e1638d3bdf1460b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Jan 2024 22:00:21 +0100 Subject: [PATCH 68/86] Add attribute to mark unsafe-eval actions. --- .../IContentSecurityPolicyProvider.cs | 7 ++++ .../Extensions/StringExtensions.cs | 7 ++++ .../Security/OrchardCoreBuilderExtensions.cs | 1 + ...lAttributeContentSecurityPolicyProvider.cs | 39 +++++++++++++++++++ .../VueContentSecurityPolicyProvider.cs | 5 ++- 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs index 20ab5c86..31073597 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/IContentSecurityPolicyProvider.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using System.Collections.Generic; using System.Threading.Tasks; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; namespace Lombiq.HelpfulLibraries.AspNetCore.Security; @@ -16,4 +17,10 @@ public interface IContentSecurityPolicyProvider /// directives names and the values are the matching directive values. /// public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context); + + /// + /// Returns the directive called or or an empty string. + /// + public static string GetDirective(IDictionary securityPolicies, string name) => + securityPolicies.GetMaybe(name) ?? securityPolicies.GetMaybe(DefaultSrc) ?? string.Empty; } diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs index 98cf0b8b..99c89e6e 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs @@ -397,4 +397,11 @@ private static (string? Left, string? Separator, string? Right) Partition( var end = index + separator.Length; return (text![..index], text[index..end], text[end..]); } + + public static string MergeWordSets(this string words, params string[] otherWords) => + string.Join( + separator: ' ', + $"{words} {string.Join(separator: ' ', otherWords)}" + .Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Distinct()); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index ed433b97..9ec38e3c 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -67,6 +67,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( services => services .AddContentSecurityPolicyProvider() .AddContentSecurityPolicyProvider() + .AddContentSecurityPolicyProvider() .ConfigureSessionCookieAlwaysSecure(), (app, _, serviceProvider) => { diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs new file mode 100644 index 00000000..60dbfd2e --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs @@ -0,0 +1,39 @@ +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 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; + } +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs index 4a26f711..f9d64ebd 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs @@ -1,6 +1,7 @@ using Lombiq.HelpfulLibraries.AspNetCore.Security; using Microsoft.AspNetCore.Http; using OrchardCore.ResourceManagement; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -25,7 +26,9 @@ public ValueTask UpdateAsync(IDictionary securityPolicies, HttpC { if (_resourceManager.GetRequiredResources("script").Any(script => script.Resource.Name == "vuejs")) { - securityPolicies[ScriptSrc] += ' ' + UnsafeEval; + securityPolicies[ScriptSrc] = IContentSecurityPolicyProvider + .GetDirective(securityPolicies, ScriptSrc) + .MergeWordSets(UnsafeEval); } return ValueTask.CompletedTask; From 71fe999be66be016ab220213d5457b48c0e009a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Jan 2024 22:10:11 +0100 Subject: [PATCH 69/86] Code styling. --- .../ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs index 60dbfd2e..c3d21d3c 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ScriptUnsafeEvalAttributeContentSecurityPolicyProvider.cs @@ -16,11 +16,10 @@ 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 class ScriptUnsafeEvalAttribute : Attribute +public sealed class ScriptUnsafeEvalAttribute : Attribute { } - public class ScriptUnsafeEvalAttributeContentSecurityPolicyProvider : IContentSecurityPolicyProvider { public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) 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 70/86] 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; - } -} From 6774eb6d04fc99f767ca55d0929ed4213a784699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Jan 2024 23:13:17 +0100 Subject: [PATCH 71/86] Add class doc and simplify pattern. --- ...SecurityPolicyAttributeContentSecurityPolicyProvider.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs index 76e77df8..7c8c3b59 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs @@ -53,12 +53,15 @@ public ContentSecurityPolicyAttribute(string directiveValue, params string[] dir } } +/// +/// Updates the content security policy based on applied to the MVC action. +/// public class ContentSecurityPolicyAttributeContentSecurityPolicyProvider : IContentSecurityPolicyProvider { public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { - if (context.RequestServices.GetService() is { ActionContext: { } actionContext } && - actionContext.ActionDescriptor is ControllerActionDescriptor actionDescriptor) + if (context.RequestServices.GetService() is + { ActionContext.ActionDescriptor: ControllerActionDescriptor actionDescriptor }) { foreach (var attribute in actionDescriptor.MethodInfo.GetCustomAttributes()) { From 0b6e36608007bf96f96ebe514c948b91a6b90cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Jan 2024 23:45:35 +0100 Subject: [PATCH 72/86] Add ResourceManagerContentSecurityPolicyProvider. --- ...rceManagerContentSecurityPolicyProvider.cs | 39 +++++++++++++++++++ .../VueContentSecurityPolicyProvider.cs | 31 +++------------ 2 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs new file mode 100644 index 00000000..deef8cf4 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs @@ -0,0 +1,39 @@ +using Lombiq.HelpfulLibraries.AspNetCore.Security; +using Microsoft.AspNetCore.Http; +using OrchardCore.ResourceManagement; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Looks in the resource manager for a resource of type called . +/// If found, the directive is amended with the value or values in . The refers to the resolution order where to look for the +/// existing directive values. Its first item is the . +/// +public abstract class ResourceManagerContentSecurityPolicyProvider : IContentSecurityPolicyProvider +{ + protected abstract string ResourceType { get; } + protected abstract string ResourceName { get; } + protected abstract string[] DirectiveNameChain { get; } + protected abstract string DirectiveValue { get; } + + private string DirectiveName => DirectiveNameChain[0]; + + public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) + { + var resourceManager = context.RequestServices.GetRequiredService(); + + if (resourceManager.GetRequiredResources(ResourceType).Any(script => script.Resource.Name == ResourceName)) + { + securityPolicies[DirectiveName] = IContentSecurityPolicyProvider + .GetDirective(securityPolicies, DirectiveNameChain) + .MergeWordSets(DirectiveValue); + } + + return ValueTask.CompletedTask; + } +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs index f9d64ebd..95e146d8 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs @@ -1,11 +1,4 @@ -using Lombiq.HelpfulLibraries.AspNetCore.Security; -using Microsoft.AspNetCore.Http; -using OrchardCore.ResourceManagement; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; namespace Microsoft.Extensions.DependencyInjection; @@ -15,22 +8,10 @@ namespace Microsoft.Extensions.DependencyInjection; /// dynamic (not precompiled) templates. These are extensively used in stock Orchard Core. Also in many third party /// modules where the DOM HTML template may contain Razor generated content. /// -public class VueContentSecurityPolicyProvider : IContentSecurityPolicyProvider +public class VueContentSecurityPolicyProvider : ResourceManagerContentSecurityPolicyProvider { - private readonly IResourceManager _resourceManager; - - public VueContentSecurityPolicyProvider(IResourceManager resourceManager) => - _resourceManager = resourceManager; - - public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) - { - if (_resourceManager.GetRequiredResources("script").Any(script => script.Resource.Name == "vuejs")) - { - securityPolicies[ScriptSrc] = IContentSecurityPolicyProvider - .GetDirective(securityPolicies, ScriptSrc) - .MergeWordSets(UnsafeEval); - } - - return ValueTask.CompletedTask; - } + protected override string ResourceType => "script"; + protected override string ResourceName => "vuejs"; + protected override string[] DirectiveNameChain { get; } = { ScriptSrc }; + protected override string DirectiveValue => UnsafeEval; } From 7d94e89ee9884732caa22e96e7a2e5730020fd22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Jan 2024 23:54:17 +0100 Subject: [PATCH 73/86] Code styling. --- .../ResourceManagerContentSecurityPolicyProvider.cs | 6 +++--- .../Security/VueContentSecurityPolicyProvider.cs | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs index deef8cf4..55cba707 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs @@ -18,10 +18,10 @@ public abstract class ResourceManagerContentSecurityPolicyProvider : IContentSec { protected abstract string ResourceType { get; } protected abstract string ResourceName { get; } - protected abstract string[] DirectiveNameChain { get; } + protected abstract IReadOnlyCollection DirectiveNameChain { get; } protected abstract string DirectiveValue { get; } - private string DirectiveName => DirectiveNameChain[0]; + private string DirectiveName => DirectiveNameChain.First(); public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { @@ -30,7 +30,7 @@ public ValueTask UpdateAsync(IDictionary securityPolicies, HttpC if (resourceManager.GetRequiredResources(ResourceType).Any(script => script.Resource.Name == ResourceName)) { securityPolicies[DirectiveName] = IContentSecurityPolicyProvider - .GetDirective(securityPolicies, DirectiveNameChain) + .GetDirective(securityPolicies, DirectiveNameChain.ToArray()) .MergeWordSets(DirectiveValue); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs index 95e146d8..0f3087c7 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/VueContentSecurityPolicyProvider.cs @@ -1,4 +1,5 @@ -using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; +using System.Collections.Generic; +using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives.CommonValues; namespace Microsoft.Extensions.DependencyInjection; @@ -12,6 +13,6 @@ public class VueContentSecurityPolicyProvider : ResourceManagerContentSecurityPo { protected override string ResourceType => "script"; protected override string ResourceName => "vuejs"; - protected override string[] DirectiveNameChain { get; } = { ScriptSrc }; + protected override IReadOnlyCollection DirectiveNameChain { get; } = new[] { ScriptSrc }; protected override string DirectiveValue => UnsafeEval; } From 591441c7ce6acf36cbdf2f6fa6604409aba3ce08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Thu, 11 Jan 2024 00:04:49 +0100 Subject: [PATCH 74/86] ThenUpdateAsync. --- .../Security/ResourceManagerContentSecurityPolicyProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs index 55cba707..a85b0541 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs @@ -32,8 +32,13 @@ public ValueTask UpdateAsync(IDictionary securityPolicies, HttpC securityPolicies[DirectiveName] = IContentSecurityPolicyProvider .GetDirective(securityPolicies, DirectiveNameChain.ToArray()) .MergeWordSets(DirectiveValue); + + return ThenUpdateAsync(securityPolicies, context); } return ValueTask.CompletedTask; } + + protected ValueTask ThenUpdateAsync(IDictionary securityPolicies, HttpContext context) => + ValueTask.CompletedTask; } From 54b9d2349c5e1c5db82f6f6688d9dbdbf9652ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Thu, 11 Jan 2024 00:05:16 +0100 Subject: [PATCH 75/86] make it virtual --- .../Security/ResourceManagerContentSecurityPolicyProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs index a85b0541..760b53ba 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs @@ -39,6 +39,6 @@ public ValueTask UpdateAsync(IDictionary securityPolicies, HttpC return ValueTask.CompletedTask; } - protected ValueTask ThenUpdateAsync(IDictionary securityPolicies, HttpContext context) => + protected virtual ValueTask ThenUpdateAsync(IDictionary securityPolicies, HttpContext context) => ValueTask.CompletedTask; } From 6a4233291c8d14363a77610313631f4f607c07b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Thu, 11 Jan 2024 00:08:13 +0100 Subject: [PATCH 76/86] Better ThenUpdateAsync. --- ...ourceManagerContentSecurityPolicyProvider.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs index 760b53ba..b6800e9b 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs @@ -26,19 +26,26 @@ public abstract class ResourceManagerContentSecurityPolicyProvider : IContentSec public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { var resourceManager = context.RequestServices.GetRequiredService(); + var resourceExists = resourceManager + .GetRequiredResources(ResourceType) + .Any(script => script.Resource.Name == ResourceName); - if (resourceManager.GetRequiredResources(ResourceType).Any(script => script.Resource.Name == ResourceName)) + if (resourceExists) { securityPolicies[DirectiveName] = IContentSecurityPolicyProvider .GetDirective(securityPolicies, DirectiveNameChain.ToArray()) .MergeWordSets(DirectiveValue); - - return ThenUpdateAsync(securityPolicies, context); } - return ValueTask.CompletedTask; + return ThenUpdateAsync(securityPolicies, context, resourceExists); } - protected virtual ValueTask ThenUpdateAsync(IDictionary securityPolicies, HttpContext context) => + /// + /// When overridden, this may be used for additional updates related to the resource in . + /// + protected virtual ValueTask ThenUpdateAsync( + IDictionary securityPolicies, + HttpContext context, + bool resourceExists) => ValueTask.CompletedTask; } From c9c9e1cc8105ac9221282a3da7f067c6c1058343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Thu, 11 Jan 2024 00:11:46 +0100 Subject: [PATCH 77/86] Make DirectiveName protected. --- .../Security/ResourceManagerContentSecurityPolicyProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs index b6800e9b..cbae5b7b 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ResourceManagerContentSecurityPolicyProvider.cs @@ -21,7 +21,7 @@ public abstract class ResourceManagerContentSecurityPolicyProvider : IContentSec protected abstract IReadOnlyCollection DirectiveNameChain { get; } protected abstract string DirectiveValue { get; } - private string DirectiveName => DirectiveNameChain.First(); + protected string DirectiveName => DirectiveNameChain.First(); public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { From 76162f4a4027b09cce04099f5ea6b26ab6edfcde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 12 Jan 2024 01:34:37 +0100 Subject: [PATCH 78/86] Finish comment. --- .../Security/ApplicationBuilderExtensions.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 547370ed..9982489c 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -65,9 +65,10 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( // No need to do content security policy on non-HTML responses. if (context.Response.ContentType?.ContainsOrdinalIgnoreCase(MediaTypeNames.Text.Html) != true) return; - // The thought behind this provider model is that if you need something else than the default, you should - // add a provider that only applies the additional directive on screens where it's actually needed. This way - // we maintain minimal permissions. If you need additional + // The thought behind this provider model is that if you need something else than the default, you + // should add a provider that only applies the additional directive on screens where it's actually + // needed. This way we maintain minimal permissions. Also if you need additional permissions for a + // specific action you can use the [ContentSecurityPolicyAttribute(value, name, parentName)] attribute. foreach (var provider in context.RequestServices.GetServices()) { await provider.UpdateAsync(securityPolicies, context); From 4193052d4b4e90b5df2d29096f63f7f421d91cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 12 Jan 2024 01:35:42 +0100 Subject: [PATCH 79/86] Update Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- .../Security/ApplicationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs index 9982489c..082a8c75 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Security/ApplicationBuilderExtensions.cs @@ -89,7 +89,7 @@ public static IApplicationBuilder UseContentSecurityPolicyHeader( /// Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response /// body to be interpreted and displayed as a content type other than the declared content type. Current (early /// 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing - /// MIME-sniffing." As written in the documentation. + /// MIME-sniffing." As written in the documentation. /// public static IApplicationBuilder UseNosniffContentTypeOptionsHeader(this IApplicationBuilder app) => app.Use(async (context, next) => From f9993f560d84b0aacabff61a5237b13372733b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 12 Jan 2024 01:35:49 +0100 Subject: [PATCH 80/86] Update Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- .../Security/OrchardCoreBuilderExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 2cbed769..770fc620 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -84,6 +84,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( .UseStrictAndSecureCookies(); }, order: 99); // Makes this service load fairly late. This should make the setup detection more accurate. + return builder .ConfigureAntiForgeryAlwaysSecure() .AddTenantFeatures("OrchardCore.Diagnostics"); From 29d34d820172902de1f02a0e9cd991caa8529c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 12 Jan 2024 01:46:39 +0100 Subject: [PATCH 81/86] Documentation cross-linking. --- Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md | 2 ++ Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md index 4a94902e..6abb72f4 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md +++ b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md @@ -9,3 +9,5 @@ - `ServiceCollectionExtensions`: Extensions methods for `IServiceCollection`, e.g. `AddContentSecurityPolicyProvider()` is a shortcut to register `IContentSecurityPolicyProvider` in dependency injection. There is a similar section for security extensions related to Orchard Core [here](../../Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md). + +These extensions provide additional security and can resolve issues reported by the [ZAP security scanner](https://github.com/Lombiq/UI-Testing-Toolbox/blob/dev/Lombiq.Tests.UI/Docs/SecurityScanning.md). diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md index 53658eba..705da267 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md @@ -5,3 +5,5 @@ - `SecurityOrchardCoreBuilderExtensions`: Adds `BuilderExtensions` extensions. For example, the `ConfigureSecurityDefaults()` that provides some default security configuration for Orchard Core. There is a similar section for security extensions related to ASP.NET Core [here](../../Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md). + +These extensions provide additional security and can resolve issues reported by the [ZAP security scanner](https://github.com/Lombiq/UI-Testing-Toolbox/blob/dev/Lombiq.Tests.UI/Docs/SecurityScanning.md). From 520cd28c1fd70a50ceabf1d867103e313b7d7f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 12 Jan 2024 01:52:17 +0100 Subject: [PATCH 82/86] Document MergeWordSets. --- .../Extensions/StringExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs index 99c89e6e..b8ae0dff 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs @@ -398,6 +398,11 @@ private static (string? Left, string? Separator, string? Right) Partition( return (text![..index], text[index..end], text[end..]); } + /// + /// Combines all provided parameters into a single string and eliminates duplicates. This can be used to get the + /// union of space separated word lists. For example it's used to build the values of individual directives in the + /// Content-Security-Policy HTTP header. + /// public static string MergeWordSets(this string words, params string[] otherWords) => string.Join( separator: ' ', From b68e2de60c74f5dac83b2d2af80533ee2d393560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 12 Jan 2024 04:29:02 +0100 Subject: [PATCH 83/86] Code styling. --- .../Security/OrchardCoreBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 770fc620..31255bfa 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -84,7 +84,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( .UseStrictAndSecureCookies(); }, order: 99); // Makes this service load fairly late. This should make the setup detection more accurate. - + return builder .ConfigureAntiForgeryAlwaysSecure() .AddTenantFeatures("OrchardCore.Diagnostics"); From fc86b3c2680b263c242203dcf4d971b1733c66d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 12 Jan 2024 11:33:08 +0100 Subject: [PATCH 84/86] Add ConfigureSecurityDefaultsWithStaticFiles. --- .../Security/OrchardCoreBuilderExtensions.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 31255bfa..3c3d23e1 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.StaticFiles; using OrchardCore.Environment.Shell; using OrchardCore.Environment.Shell.Models; using System.Linq; @@ -61,7 +62,26 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo public static OrchardCoreBuilder ConfigureSecurityDefaults( this OrchardCoreBuilder builder, bool allowInlineScript = true, - bool allowInlineStyle = false) + bool allowInlineStyle = false) => + builder.ConfigureSecurityDefaultsInner(allowInlineScript, allowInlineStyle, useStaticFiles: false); + + /// + /// The same as , but also registers the + /// at the end of the chain. It's important to not do this earlier (e.g. with app.UseStaticFiles() because + /// it short-circuits the call chain when delivering static files so later middlewares are not executed and so the + /// X-Content-Type-Options: nosniff header doesn't get applied to those files. + /// + public static OrchardCoreBuilder ConfigureSecurityDefaultsWithStaticFiles( + this OrchardCoreBuilder builder, + bool allowInlineScript = true, + bool allowInlineStyle = false) => + builder.ConfigureSecurityDefaultsInner(allowInlineScript, allowInlineStyle, useStaticFiles: true); + + private static OrchardCoreBuilder ConfigureSecurityDefaultsInner( + this OrchardCoreBuilder builder, + bool allowInlineScript, + bool allowInlineStyle, + bool useStaticFiles) { builder.ApplicationServices.AddInlineStartup( services => services @@ -82,6 +102,8 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( .UseContentSecurityPolicyHeader(allowInlineScript, allowInlineStyle) .UseNosniffContentTypeOptionsHeader() .UseStrictAndSecureCookies(); + + if (useStaticFiles) app.UseStaticFiles(); }, order: 99); // Makes this service load fairly late. This should make the setup detection more accurate. From 764c93ad727bb22c62f3f5bd847f6c054ba97e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 12 Jan 2024 12:20:55 +0100 Subject: [PATCH 85/86] Update Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs index b8ae0dff..4e814797 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs @@ -403,6 +403,10 @@ private static (string? Left, string? Separator, string? Right) Partition( /// union of space separated word lists. For example it's used to build the values of individual directives in the /// Content-Security-Policy HTTP header. /// + /// + /// Given the words "script-src 'self'" and otherWords containing "script-src example.com", the result would be + /// "script-src 'self' example.com". + /// public static string MergeWordSets(this string words, params string[] otherWords) => string.Join( separator: ' ', From dc9180c7a214e93c5e344394b1e44f5f3262196e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sun, 14 Jan 2024 21:56:04 +0100 Subject: [PATCH 86/86] Rephrase docstring. --- .../Security/OrchardCoreBuilderExtensions.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs index 3c3d23e1..a285fcca 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/OrchardCoreBuilderExtensions.cs @@ -24,6 +24,7 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo /// Provides some default security configuration for Orchard Core. /// /// + /// This extension method configures the application as listed below. /// /// /// @@ -58,6 +59,11 @@ public static OrchardCoreBuilder ConfigureAntiForgeryAlwaysSecure(this OrchardCo /// /// /// + /// + /// If you also need static file support, consider using + /// instead. Alternatively, make sure to put the app.UseStaticFiles() call at the very end of your app + /// configuration chain so it won't short-circuit prematurely and miss adding security headers to your static files. + /// /// public static OrchardCoreBuilder ConfigureSecurityDefaults( this OrchardCoreBuilder builder, @@ -67,9 +73,9 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults( /// /// The same as , but also registers the - /// at the end of the chain. It's important to not do this earlier (e.g. with app.UseStaticFiles() because - /// it short-circuits the call chain when delivering static files so later middlewares are not executed and so the - /// X-Content-Type-Options: nosniff header doesn't get applied to those files. + /// at the end of the chain, so app.UseStaticFiles() should not be called when this is used. This is helpful + /// because short-circuits the call chain when delivering static files, so later + /// middlewares are not executed (e.g. the X-Content-Type-Options: nosniff header wouldn't be added). /// public static OrchardCoreBuilder ConfigureSecurityDefaultsWithStaticFiles( this OrchardCoreBuilder builder,