Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LMBQ-267: Adding ContentSecurityPolicyProviders for embedded media and external login providers, ReCaptcha, Google Analytics #244

Merged
merged 23 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
abfd380
Code styling
Piedone Mar 8, 2024
513482a
Merge branch 'issue/LMBQ-307' into issue/LMBQ-267
Piedone Mar 8, 2024
4d0fec5
Adding IContentSecurityPolicyProvider for frame-src
Piedone Mar 8, 2024
8e860f3
Renaming FrameSourceContentSecurityPolicyProvider
Piedone Mar 8, 2024
cb7a01e
Docs
Piedone Mar 8, 2024
0a8e611
Adding ExternalLoginContentSecurityPolicyProvider
Piedone Mar 8, 2024
73187d3
Fixing double spaces in Markdown
Piedone Mar 8, 2024
19ca488
Typo
Piedone Mar 8, 2024
9bb310f
Merge remote-tracking branch 'origin/dev' into issue/LMBQ-267
Piedone Mar 10, 2024
c8a3425
Merge remote-tracking branch 'origin/dev' into issue/LMBQ-267
Piedone Mar 18, 2024
587c135
Merge remote-tracking branch 'origin/dev' into issue/LMBQ-267
Piedone Mar 24, 2024
e011f2c
Docs
Piedone Mar 28, 2024
c38bc0a
Merge remote-tracking branch 'origin/dev' into issue/LMBQ-267
Piedone Mar 28, 2024
e9695f9
Docs
Piedone Mar 28, 2024
ef5039c
Adding CSP directives for ReCaptcha
Piedone Mar 28, 2024
51b9353
Only adding CSP directives for ReCaptcha when it's enabled
Piedone Mar 28, 2024
571fe6f
Moving CSP directive sources to strings, so sources other than valid …
Piedone Mar 28, 2024
4e89530
Adding GoogleAnalyticsContentSecurityPolicyProvider, docs
Piedone Mar 28, 2024
b74e905
Sealed of approval
Piedone Mar 28, 2024
7320b31
Spelling
Piedone Mar 28, 2024
da1ffe3
More spelling
Piedone Mar 28, 2024
861d8dc
Moving ExternalLoginContentSecurityPolicyProvider so it's only enable…
Piedone Mar 28, 2024
3615945
DRY feature operations
Piedone Apr 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Lombiq.HelpfulLibraries.AspNetCore/Docs/Security.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- `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.
- `EmbeddedMediaContentSecurityPolicyProvider`: An optional policy provider that permits additional host names used by usual media embedding sources (like YouTube) for the `frame-scr` directive.
- `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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static class ApplicationBuilderExtensions
/// </param>
/// <param name="allowInlineStyle">
/// If <see langword="true"/> 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.
/// blocks and no style attributes, some JavaScript libraries may still create some from code.
/// </param>
[SuppressMessage(
"Critical Code Smell",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -16,77 +15,79 @@ namespace Lombiq.HelpfulLibraries.AspNetCore.Security;
public class CdnContentSecurityPolicyProvider : IContentSecurityPolicyProvider
{
/// <summary>
/// Gets the URLs whose <see cref="Uri.Host"/> will be added to the <see cref="StyleSrc"/> directive.
/// Gets the sources that will be added to the <see cref="StyleSrc"/> directive.
/// </summary>
public static ConcurrentBag<Uri> PermittedStyleSources { get; } = new(new[]
public static ConcurrentBag<string> PermittedStyleSources { get; } = new(new[]
{
new Uri("https://fonts.googleapis.com/css"),
new Uri("https://fonts.gstatic.com/"),
new Uri("https://cdn.jsdelivr.net/npm"),
new Uri("https://fastly.jsdelivr.net/npm"),
new Uri("https://cdnjs.cloudflare.com/"),
new Uri("https://maxcdn.bootstrapcdn.com/"),
"fonts.googleapis.com",
"fonts.gstatic.com", // #spell-check-ignore-line
"cdn.jsdelivr.net", // #spell-check-ignore-line
"fastly.jsdelivr.net", // #spell-check-ignore-line
"cdnjs.cloudflare.com", // #spell-check-ignore-line
"maxcdn.bootstrapcdn.com", // #spell-check-ignore-line
});

/// <summary>
/// Gets the URLs whose <see cref="Uri.Host"/> will be added to the <see cref="ScriptSrc"/> directive.
/// Gets the sources that will be added to the <see cref="ScriptSrc"/> directive.
/// </summary>
public static ConcurrentBag<Uri> PermittedScriptSources { get; } = new(new[]
public static ConcurrentBag<string> PermittedScriptSources { get; } = new(new[]
{
new Uri("https://cdn.jsdelivr.net/npm"),
new Uri("https://code.jquery.com/"),
new Uri("https://fastly.jsdelivr.net/npm"),
new Uri("https://cdnjs.cloudflare.com/"),
new Uri("https://maxcdn.bootstrapcdn.com/"),
"cdn.jsdelivr.net", // #spell-check-ignore-line
"cdnjs.cloudflare.com", // #spell-check-ignore-line
"code.jquery.com",
"fastly.jsdelivr.net", // #spell-check-ignore-line
"maxcdn.bootstrapcdn.com", // #spell-check-ignore-line
});

/// <summary>
/// Gets the URLs whose <see cref="Uri.Host"/> will be added to the <see cref="FontSrc"/> directive.
/// Gets the sources that will be added to the <see cref="FontSrc"/> directive.
/// </summary>
public static ConcurrentBag<Uri> PermittedFontSources { get; } = new(new[]
public static ConcurrentBag<string> PermittedFontSources { get; } = new(new[]
{
new Uri("https://fonts.googleapis.com/"),
new Uri("https://fonts.gstatic.com/"),
"cdn.jsdelivr.net", // #spell-check-ignore-line
"fonts.googleapis.com",
"fonts.gstatic.com", // #spell-check-ignore-line
});

/// <summary>
/// Gets the sources that will be added to the <see cref="FrameSrc"/> directive.
/// </summary>
public static ConcurrentBag<string> PermittedFrameSources { get; } = [];

public ValueTask UpdateAsync(IDictionary<string, string> securityPolicies, HttpContext context)
{
var any = false;

if (!PermittedStyleSources.IsEmpty)
{
any = true;
MergeValues(securityPolicies, StyleSrc, PermittedStyleSources);
CspHelper.MergeValues(securityPolicies, StyleSrc, PermittedStyleSources);
}

if (!PermittedScriptSources.IsEmpty)
{
any = true;
MergeValues(securityPolicies, ScriptSrc, PermittedScriptSources);
CspHelper.MergeValues(securityPolicies, ScriptSrc, PermittedScriptSources);
}

if (!PermittedFontSources.IsEmpty)
{
any = true;
MergeValues(securityPolicies, FontSrc, PermittedFontSources);
CspHelper.MergeValues(securityPolicies, FontSrc, PermittedFontSources);
}

if (!PermittedFrameSources.IsEmpty)
{
any = true;
CspHelper.MergeValues(securityPolicies, FrameSrc, PermittedFrameSources);
}

if (any)
{
var allPermittedSources = PermittedStyleSources.Concat(PermittedScriptSources).Concat(PermittedFontSources);
MergeValues(securityPolicies, ConnectSrc, allPermittedSources);
CspHelper.MergeValues(securityPolicies, ConnectSrc, allPermittedSources);
}

return ValueTask.CompletedTask;
}

private static void MergeValues(IDictionary<string, string> policies, string key, IEnumerable<Uri> sources)
{
var directiveValue = policies.GetMaybe(key) ?? policies.GetMaybe(DefaultSrc) ?? string.Empty;

policies[key] = string.Join(' ', directiveValue
.Split(' ')
.Union(sources.Select(uri => uri.Host))
.Distinct());
}
}
20 changes: 20 additions & 0 deletions Lombiq.HelpfulLibraries.AspNetCore/Security/CspHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Linq;

namespace Lombiq.HelpfulLibraries.AspNetCore.Security;

public static class CspHelper
{
public static void MergeValues(IDictionary<string, string> policies, string key, params string[] sources) =>
MergeValues(policies, key, (IEnumerable<string>)sources);

public static void MergeValues(IDictionary<string, string> policies, string key, IEnumerable<string> sources)
{
var directiveValue = policies.GetMaybe(key) ?? policies.GetMaybe(ContentSecurityPolicyDirectives.DefaultSrc) ?? string.Empty;

policies[key] = string.Join(' ', directiveValue
.Split(' ')
.Union(sources)
.Distinct());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Http;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives;

namespace Lombiq.HelpfulLibraries.AspNetCore.Security;

/// <summary>
/// A content security policy directive provider that provides additional permitted host names used by usual media
/// embedding sources (like YouTube) for <see cref="FrameSrc"/>.
/// </summary>
public class EmbeddedMediaContentSecurityPolicyProvider : IContentSecurityPolicyProvider
{
/// <summary>
/// Gets the sources that will be added to the <see cref="FrameSrc"/> directive.
/// </summary>
public static ConcurrentBag<string> PermittedFrameSources { get; } = new(new[]
{
"www.youtube.com/",
"www.youtube-nocookie.com/",
});

public ValueTask UpdateAsync(IDictionary<string, string> securityPolicies, HttpContext context)
{
if (!PermittedFrameSources.IsEmpty)
{
CspHelper.MergeValues(securityPolicies, FrameSrc, PermittedFrameSources);
}

return ValueTask.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -46,7 +46,7 @@ public static class ContentSecurityPolicyProvider
public static string GetDirective(IDictionary<string, string> securityPolicies, params string[] names) =>
GetDirective(securityPolicies, names.AsEnumerable());

/// <inheritdoc cref="GetDirective(System.Collections.Generic.IDictionary{string,string},string[])"/>
/// <inheritdoc cref="GetDirective(IDictionary{string,string},string[])"/>
public static string GetDirective(IDictionary<string, string> securityPolicies, IEnumerable<string> names)
{
foreach (var name in names)
Expand Down
2 changes: 2 additions & 0 deletions Lombiq.HelpfulLibraries.OrchardCore/Docs/Environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

## Extensions

- `FeatureInfoEnumerableExtensions`: Shortcuts for `IEnumerable<IFeatureInfo>`, like `Any(featureId)`.
- `OrchardCoreBuilderExtensions`: Shortcuts that can be used when initializing Orchard with `OrchardCoreBuilder`, e.g. `AddOrchardCms()`.
- `ShellFeaturesManagerExtensions`: Shortcuts for `IShellFeaturesManager`, like `IsFeatureEnabledAsync()`.
3 changes: 3 additions & 0 deletions Lombiq.HelpfulLibraries.OrchardCore/Docs/Security.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ These extensions provide additional security and can resolve issues reported by

## Services

- `ExternalLoginContentSecurityPolicyProvider`: Provides various directives for the `Content-Security-Policy` header, allowing using external login providers that require special headers (like Microsoft login). Is automatically enabled when the affected features are enabled.
- `GoogleAnalyticsContentSecurityPolicyProvider`: Provides various directives for the `Content-Security-Policy` header, allowing using Google Analytics tracking. Is automatically enabled when the `OrchardCore.Google.Analytics` feature is enabled or the provider is explicitly enabled for the current request via is `static` method.
- `ReCaptchaContentSecurityPolicyProvider`: Provides various directives for the `Content-Security-Policy` header, allowing using ReCaptcha captchas. Is automatically enabled when the `OrchardCore.ReCaptcha` feature is enabled.
- `ResourceManagerContentSecurityPolicyProvider`: An abstract base class for implementing content security policy providers that trigger when the specified resource is included.
- `VueContentSecurityPolicyProvider`: An implementation of `ResourceManagerContentSecurityPolicyProvider` that adds `script-src: unsafe-eval` permission to the page if it uses the `vuejs` resource. This includes any Vue.js app in stock Orchard Core, apps you create in your view files, and SFCs created with the Lombiq.VueJs module. This is necessary, because without `unsafe-eval` Vue.js only supports templates that are pre-compiled into JS code.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using OrchardCore.Environment.Extensions.Features;
using System.Collections.Generic;
using System.Linq;

namespace OrchardCore.Environment.Shell;

/// <summary>
/// Shortcuts for <see cref="IEnumerable{IFeatureInfo}"/>.
/// </summary>
public static class FeatureInfoEnumerableExtensions
{
/// <summary>
/// Checks whether the given <see cref="IEnumerable{IFeatureInfo}"/> contains a feature with the given technical ID.
/// </summary>
/// <param name="featureId">Technical ID of the module feature.</param>
public static bool Any(this IEnumerable<IFeatureInfo> featureInfos, string featureId) =>
featureInfos.Any(feature => feature.Id == featureId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Threading.Tasks;

namespace OrchardCore.Environment.Shell;

/// <summary>
/// Shortcuts for <see cref="IShellFeaturesManager"/>.
/// </summary>
public static class ShellFeaturesManagerExtensions
{
/// <summary>
/// Checks whether the given module feature is enabled for the current shell.
/// </summary>
/// <param name="featureId">Technical ID of the module feature.</param>
public static async Task<bool> IsFeatureEnabledAsync(this IShellFeaturesManager shellFeaturesManager, string featureId) =>
(await shellFeaturesManager.GetEnabledFeaturesAsync()).Any(featureId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Lombiq.HelpfulLibraries.AspNetCore.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Environment.Shell;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives;

namespace Lombiq.HelpfulLibraries.OrchardCore.Security;

internal sealed class ExternalLoginContentSecurityPolicyProvider : IContentSecurityPolicyProvider
{
public async ValueTask UpdateAsync(IDictionary<string, string> securityPolicies, HttpContext context)
{
var shellFeaturesManager = context.RequestServices.GetRequiredService<IShellFeaturesManager>();
var enabledFeatures = await shellFeaturesManager.GetEnabledFeaturesAsync();
DemeSzabolcs marked this conversation as resolved.
Show resolved Hide resolved

if (enabledFeatures.Any("OrchardCore.Microsoft.Authentication.AzureAD"))
{
CspHelper.MergeValues(securityPolicies, FormAction, "login.microsoftonline.com"); // #spell-check-ignore-line
}

if (enabledFeatures.Any("OrchardCore.GitHub.Authentication"))
{
CspHelper.MergeValues(securityPolicies, FormAction, "github.com");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Lombiq.HelpfulLibraries.AspNetCore.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Environment.Shell;
using System.Collections.Generic;
using System.Threading.Tasks;

using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives;

namespace Lombiq.HelpfulLibraries.OrchardCore.Security;

public class GoogleAnalyticsContentSecurityPolicyProvider : IContentSecurityPolicyProvider
{
private const string HttpContextItemKey = nameof(GoogleAnalyticsContentSecurityPolicyProvider);

public async ValueTask UpdateAsync(IDictionary<string, string> securityPolicies, HttpContext context)
{
var googleAnalyticsIsEnabled = context.Items.ContainsKey(HttpContextItemKey);

if (!googleAnalyticsIsEnabled)
{
var shellFeaturesManager = context.RequestServices.GetRequiredService<IShellFeaturesManager>();
googleAnalyticsIsEnabled = await shellFeaturesManager.IsFeatureEnabledAsync("OrchardCore.Google.Analytics");
}

if (googleAnalyticsIsEnabled)
{
CspHelper.MergeValues(securityPolicies, ScriptSrc, "www.googletagmanager.com");
}
}

public static void EnableForCurrentRequest(HttpContext context) => context.Items[HttpContextItemKey] = "enabled";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Lombiq.HelpfulLibraries.AspNetCore.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Environment.Shell;
using System.Collections.Generic;
using System.Threading.Tasks;

using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives;

namespace Lombiq.HelpfulLibraries.OrchardCore.Security;

internal sealed class ReCaptchaContentSecurityPolicyProvider : IContentSecurityPolicyProvider
{
public async ValueTask UpdateAsync(IDictionary<string, string> securityPolicies, HttpContext context)
{
var shellFeaturesManager = context.RequestServices.GetRequiredService<IShellFeaturesManager>();

if (await shellFeaturesManager.IsFeatureEnabledAsync("OrchardCore.ReCaptcha"))
{
CspHelper.MergeValues(securityPolicies, ScriptSrc, "www.google.com", "www.gstatic.com");
CspHelper.MergeValues(securityPolicies, FrameSrc, "www.google.com");
}
}
}
Loading