Skip to content

Commit

Permalink
Add ContentSecurityPolicyAttribute.
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahelsaig committed Jan 10, 2024
1 parent 71fe999 commit 8d6aee7
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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:";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,19 @@ public interface IContentSecurityPolicyProvider
public ValueTask UpdateAsync(IDictionary<string, string> securityPolicies, HttpContext context);

/// <summary>
/// Returns the directive called <paramref name="name"/> or <see cref="DefaultSrc"/> or an empty string.
/// Returns the first non-empty directive from the <paramref name="names"/> or <see cref="DefaultSrc"/> or an empty
/// string.
/// </summary>
public static string GetDirective(IDictionary<string, string> securityPolicies, string name) =>
securityPolicies.GetMaybe(name) ?? securityPolicies.GetMaybe(DefaultSrc) ?? string.Empty;
public static string GetDirective(IDictionary<string, string> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Indicates that the action's view should have the <c>script-src: unsafe-eval</c> content security policy directive.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class ScriptUnsafeEvalAttribute : ContentSecurityPolicyAttribute
{
public ScriptUnsafeEvalAttribute()
: base(UnsafeEval, ScriptSrc)
{
}
}

/// <summary>
/// Indicates that the action's view should have the provided content security policy directive.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
[SuppressMessage(
"Performance",
"CA1813:Avoid unsealed attributes",
Justification = $"Inherited by {nameof(ScriptUnsafeEvalAttribute)}.")]
public class ContentSecurityPolicyAttribute : Attribute
{
/// <summary>
/// Gets the fallback chain of the directive, excluding <see cref="DefaultSrc"/>. This is used to get the current
/// value.
/// </summary>
public string[] DirectiveNames { get; }

/// <summary>
/// Gets the value to be added to the directive. The content is split into words and added to the current values
/// without repetition.
/// </summary>
public string DirectiveValue { get; }

public ContentSecurityPolicyAttribute(string directiveValue, params string[] directiveNames)
{
DirectiveValue = directiveValue;
DirectiveNames = directiveNames;
}
}

public class ContentSecurityPolicyAttributeContentSecurityPolicyProvider : IContentSecurityPolicyProvider
{
public ValueTask UpdateAsync(IDictionary<string, string> securityPolicies, HttpContext context)
{
if (context.RequestServices.GetService<IActionContextAccessor>() is { ActionContext: { } actionContext } &&
actionContext.ActionDescriptor is ControllerActionDescriptor actionDescriptor)
{
foreach (var attribute in actionDescriptor.MethodInfo.GetCustomAttributes<ContentSecurityPolicyAttribute>())
{
securityPolicies[ScriptSrc] = IContentSecurityPolicyProvider
.GetDirective(securityPolicies, attribute.DirectiveNames)
.MergeWordSets(attribute.DirectiveValue);
}
}

return ValueTask.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public static OrchardCoreBuilder ConfigureSecurityDefaults(
services => services
.AddContentSecurityPolicyProvider<CdnContentSecurityPolicyProvider>()
.AddContentSecurityPolicyProvider<VueContentSecurityPolicyProvider>()
.AddContentSecurityPolicyProvider<ScriptUnsafeEvalAttributeContentSecurityPolicyProvider>()
.AddContentSecurityPolicyProvider<ContentSecurityPolicyAttributeContentSecurityPolicyProvider>()
.ConfigureSessionCookieAlwaysSecure(),
(app, _, serviceProvider) =>
{
Expand Down

This file was deleted.

0 comments on commit 8d6aee7

Please sign in to comment.