diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/BaseCspContributor.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/BaseCspContributor.cs new file mode 100644 index 00000000000..44b12f1738c --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/BaseCspContributor.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Base class for all CSP directive contributors. + /// + public abstract class BaseCspContributor + { + /// + /// Gets unique identifier for the contributor. + /// + public Guid Id { get; } = Guid.NewGuid(); + + /// + /// Gets or sets type of the CSP directive. + /// + public CspDirectiveType DirectiveType { get; protected set; } + + /// + /// Generates the directive string. + /// + /// The directive string. + public abstract string GenerateDirective(); + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/ContentSecurityPolicy.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ContentSecurityPolicy.cs new file mode 100644 index 00000000000..c0290111388 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ContentSecurityPolicy.cs @@ -0,0 +1,460 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System.Collections.Generic; + using System.Linq; + + /// + /// Manages the entire Content Security Policy. + /// + public class ContentSecurityPolicy : IContentSecurityPolicy + { + private string nonce; + + /// Initializes a new instance of the class. + public ContentSecurityPolicy() + { + } + + /// + /// Gets a cryptographically secure random nonce value for use in CSP policies. + /// + public string Nonce + { + get + { + if (this.nonce == null) + { + var nonceBytes = new byte[32]; + var generator = System.Security.Cryptography.RandomNumberGenerator.Create(); + generator.GetBytes(nonceBytes); + this.nonce = System.Convert.ToBase64String(nonceBytes); + } + + return this.nonce; + } + } + + /// + /// Gets the default source contributor for managing default-src directives. + /// + public SourceCspContributor DefaultSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.DefaultSrc); + } + } + + /// + /// Gets the script source contributor for managing script-src directives. + /// + public SourceCspContributor ScriptSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.ScriptSrc); + } + } + + /// + /// Gets the style source contributor for managing style-src directives. + /// + public SourceCspContributor StyleSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.StyleSrc); + } + } + + /// + /// Gets the image source contributor for managing img-src directives. + /// + public SourceCspContributor ImgSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.ImgSrc); + } + } + + /// + /// Gets the connect source contributor for managing connect-src directives. + /// + public SourceCspContributor ConnectSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.ConnectSrc); + } + } + + /// + /// Gets the frame ancestors contributor for managing frame-ancestors directives. + /// + public SourceCspContributor FrameAncestors + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.FrameAncestors); + } + } + + /// + /// Gets the font source contributor for managing font-src directives. + /// + public SourceCspContributor FontSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.FontSrc); + } + } + + /// + /// Gets the object source contributor for managing object-src directives. + /// + public SourceCspContributor ObjectSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.ObjectSrc); + } + } + + /// + /// Gets the media source contributor for managing media-src directives. + /// + public SourceCspContributor MediaSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.MediaSrc); + } + } + + /// + /// Gets the frame source contributor for managing frame-src directives. + /// + public SourceCspContributor FrameSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.FrameSrc); + } + } + + /// + /// Gets the form action source contributor for managing form-action directives. + /// + public SourceCspContributor FormAction + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.FormAction); + } + } + + /// + /// Gets the base URI source contributor for managing base-uri directives. + /// + public SourceCspContributor BaseUriSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.BaseUri); + } + } + + /// + /// Gets collection of CSP contributors for content security policy directives. + /// + private List ContentSecurityPolicyContributors { get; } = new List(); + + /// + /// Gets collection of CSP contributors for reporting endpoints directives. + /// + private List ReportingEndpointsContributors { get; } = new List(); + + /// + /// Parses a CSP header string into a ContentSecurityPolicy object. + /// + /// The CSP header string to parse. + /// A ContentSecurityPolicy object representing the parsed header. + /// Thrown when the CSP header is invalid or cannot be parsed. + public IContentSecurityPolicy AddHeader(string cspHeader) + { + var parser = new ContentSecurityPolicyParser(this); + parser.Parse(cspHeader); + return this; + } + + /// + /// Adds a reporting directive to the policy. + /// + /// The reporting directive to add. + /// A ContentSecurityPolicy object representing the parsed header. + public IContentSecurityPolicy AddReportEndpointHeader(string header) + { + if (!string.IsNullOrEmpty(header)) + { + var parser = new ContentSecurityPolicyParser(this); + parser.ParseReportingEndpoints(header); + } + + return this; + } + + /// + /// Removes script sources of the specified type from the CSP policy. + /// + /// The CSP source type to remove. + public void RemoveScriptSources(CspSourceType cspSourceType) + { + this.RemoveSources(CspDirectiveType.ScriptSrc, cspSourceType); + } + + /// + /// Adds allowed plugin types to the CSP policy. + /// + /// The plugin type to allow. + public void AddPluginTypes(string value) + { + this.AddDocumentDirective(CspDirectiveType.PluginTypes, value); + } + + /// + /// Adds a sandbox directive to the CSP policy. + /// + /// The sandbox directive value. + public void AddSandbox(string value) + { + this.SetDocumentDirective(CspDirectiveType.SandboxDirective, value); + } + + /// + /// Adds a form-action directive to the CSP policy. + /// + /// The CSP source type to add. + /// The value associated with the source. + public void AddFormAction(CspSourceType sourceType, string value) + { + this.AddSource(CspDirectiveType.FormAction, sourceType, value); + } + + /// + /// Adds a frame-ancestors directive to the CSP policy. + /// + /// The CSP source type to add. + /// The value associated with the source. + public void AddFrameAncestors(CspSourceType sourceType, string value) + { + this.AddSource(CspDirectiveType.FrameAncestors, sourceType, value); + } + + /// + /// Adds a report URI to the CSP policy. + /// + /// The name where violation reports will be sent. + /// The URI where violation reports will be sent. + public void AddReportEndpoint(string name, string value) + { + // this.AddReportingDirective(CspDirectiveType.ReportUri, value); + this.AddReportingEndpointsDirective(name, value); + } + + /// + /// Adds a report endpoint to the CSP policy. + /// + /// The endpoint where reports will be sent. + public void AddReportTo(string value) + { + this.AddReportingDirective(CspDirectiveType.ReportTo, value); + } + + /// + /// Adds the upgrade-insecure-requests directive to upgrade HTTP requests to HTTPS. + /// + public void UpgradeInsecureRequests() + { + this.SetDocumentDirective(CspDirectiveType.UpgradeInsecureRequests, string.Empty); + } + + /// + /// Generates the complete Content Security Policy. + /// + /// The complete Content Security Policy. + public string GeneratePolicy() + { + return string.Join( + "; ", + this.ContentSecurityPolicyContributors + .Select(c => c.GenerateDirective()) + .Where(d => !string.IsNullOrEmpty(d))); + } + + /// + /// Generates the complete security policy. + /// + /// Reporting Endpoints as a string. + public string GenerateReportingEndpoints() + { + return string.Join( + "; ", + this.ReportingEndpointsContributors + .Select(c => c.GenerateDirective()) + .Where(d => !string.IsNullOrEmpty(d))); + } + + /// + /// Gets an existing directive contributor or creates a new one if it doesn't exist. + /// + /// The type of directive to get or create. + /// A SourceCspContributor for the specified directive type. + private SourceCspContributor GetOrCreateDirective(CspDirectiveType directiveType) + { + var directive = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as SourceCspContributor; + if (directive == null) + { + directive = new SourceCspContributor(directiveType); + this.AddContributor(directive); + } + + return directive; + } + + /// + /// Adds a contributor to the content security policy. + /// + /// The contributor to add to the policy. + private void AddContributor(BaseCspContributor contributor) + { + // Remove any existing contributor of the same directive type + this.ContentSecurityPolicyContributors.RemoveAll(c => c.DirectiveType == contributor.DirectiveType); + this.ContentSecurityPolicyContributors.Add(contributor); + } + + /// + /// Adds a contributor to the reporting endpoints collection. + /// + /// The contributor to add to the reporting endpoints. + private void AddReportingEndpointsContributors(BaseCspContributor contributor) + { + // Remove any existing contributor of the same directive type + this.ReportingEndpointsContributors.RemoveAll(c => c.DirectiveType == contributor.DirectiveType); + this.ReportingEndpointsContributors.Add(contributor); + } + + /// + /// Adds a source to the specified directive type. + /// + /// The directive type to add the source to. + /// The type of source to add. + /// The value associated with the source. If null and sourceType is Nonce, uses the generated nonce. + private void AddSource(CspDirectiveType directiveType, CspSourceType sourceType, string value = null) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as SourceCspContributor; + if (contributor == null) + { + contributor = new SourceCspContributor(directiveType); + this.AddContributor(contributor); + } + + if (sourceType == CspSourceType.Nonce && string.IsNullOrEmpty(value)) + { + value = this.Nonce; + } + + contributor.AddSource(new CspSource(sourceType, value)); + } + + /// + /// Removes sources of the specified type from the directive. + /// + /// The directive type to remove sources from. + /// The type of sources to remove. + private void RemoveSources(CspDirectiveType directiveType, CspSourceType sourceType) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as SourceCspContributor; + if (contributor == null) + { + contributor = new SourceCspContributor(directiveType); + this.AddContributor(contributor); + } + + contributor.RemoveSources(sourceType); + } + + /// + /// Sets a document directive value, replacing any existing value. + /// + /// The directive type to set. + /// The value to set for the directive. + private void SetDocumentDirective(CspDirectiveType directiveType, string value) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as DocumentCspContributor; + if (contributor == null) + { + contributor = new DocumentCspContributor(directiveType, value); + this.AddContributor(contributor); + } + + contributor.SetDirectiveValue(value); + } + + /// + /// Adds a document directive with the specified value. + /// + /// The directive type to add. + /// The value for the directive. + private void AddDocumentDirective(CspDirectiveType directiveType, string value) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as DocumentCspContributor; + if (contributor == null) + { + contributor = new DocumentCspContributor(directiveType, value); + this.AddContributor(contributor); + } + + contributor.SetDirectiveValue(value); + } + + /// + /// Adds a reporting directive with the specified value. + /// + /// The directive type to add. + /// The value for the reporting directive. + private void AddReportingDirective(CspDirectiveType directiveType, string value) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as ReportingCspContributor; + if (contributor == null) + { + contributor = new ReportingCspContributor(directiveType); + this.AddContributor(contributor); + } + + contributor.AddReportingEndpoint(value); + } + + /// + /// Adds a reporting endpoints directive with the specified name and value. + /// + /// The name of the reporting endpoint. + /// The URI value for the reporting endpoint. + private void AddReportingEndpointsDirective(string name, string value) + { + var contributor = this.ReportingEndpointsContributors.FirstOrDefault(c => c.DirectiveType == CspDirectiveType.ReportUri) as ReportingEndpointContributor; + if (contributor == null) + { + contributor = new ReportingEndpointContributor(CspDirectiveType.ReportUri); + this.AddReportingEndpointsContributors(contributor); + } + + contributor.AddReportingEndpoint(name, value); + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/ContentSecurityPolicyParser.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ContentSecurityPolicyParser.cs new file mode 100644 index 00000000000..50ca8a2a482 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ContentSecurityPolicyParser.cs @@ -0,0 +1,302 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Utility class for parsing Content Security Policy headers into ContentSecurityPolicy objects. + /// + public class ContentSecurityPolicyParser + { + private readonly IContentSecurityPolicy policy; + + /// + /// Initializes a new instance of the class. + /// + /// The ContentSecurityPolicy instance to populate with parsed directives. + public ContentSecurityPolicyParser(IContentSecurityPolicy policy) + { + this.policy = policy ?? throw new ArgumentNullException(nameof(policy)); + } + + /// + /// Parses a CSP header string into the provided ContentSecurityPolicy object. + /// + /// The CSP header string to parse. + /// Thrown when the CSP header is invalid or cannot be parsed. + public void Parse(string cspHeader) + { + if (string.IsNullOrWhiteSpace(cspHeader)) + { + throw new ArgumentException("CSP header cannot be null or empty", nameof(cspHeader)); + } + + // Split the header into individual directives + var directives = SplitDirectives(cspHeader); + + foreach (var directive in directives) + { + this.ParseDirective(directive); + } + } + + /// + /// Tries to parse a CSP header string into the provided ContentSecurityPolicy object. + /// + /// The CSP header string to parse. + /// True if parsing was successful, false otherwise. + public bool TryParse(string cspHeader) + { + try + { + this.Parse(cspHeader); + return true; + } + catch + { + return false; + } + } + + /// + /// Parses a reporting endpoints header string into the provided ContentSecurityPolicy object. + /// + /// The CSP header string to parse. + public void ParseReportingEndpoints(string cspHeader) + { + if (string.IsNullOrWhiteSpace(cspHeader)) + { + return; + } + + // Split the header into individual directives + var directives = cspHeader.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(d => d.Trim()) + .Where(d => !string.IsNullOrEmpty(d)); + + foreach (var directive in directives) + { + this.ParseReportingEndpointsDirective(directive); + } + } + + /// + /// Splits the CSP header into individual directive strings. + /// + private static IEnumerable SplitDirectives(string cspHeader) + { + // CSP directives are separated by semicolons + return cspHeader.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) + .Select(d => d.Trim()) + .Where(d => !string.IsNullOrEmpty(d)); + } + + /// + /// Parses a source string and adds it to the contributor. + /// + private static void ParseAndAddSource(SourceCspContributor contributor, string source) + { + var trimmedSource = source.Trim(); + + // Check for quoted keywords first + if (CspSourceTypeNameMapper.IsQuotedKeyword(trimmedSource)) + { + if (CspSourceTypeNameMapper.TryGetSourceType(trimmedSource, out var sourceType)) + { + switch (sourceType) + { + case CspSourceType.Self: + contributor.AddSelf(); + break; + + case CspSourceType.Inline: + contributor.AddInline(); + break; + + case CspSourceType.Eval: + contributor.AddEval(); + break; + + case CspSourceType.None: + contributor.AddNone(); + break; + + case CspSourceType.StrictDynamic: + contributor.AddStrictDynamic(); + break; + } + } + else if (CspSourceTypeNameMapper.IsNonceSource(trimmedSource)) + { + var quotedValue = trimmedSource.Substring(1, trimmedSource.Length - 2); + var nonce = quotedValue.Substring(6); // Remove "nonce-" prefix + contributor.AddNonce(nonce); + } + else if (CspSourceTypeNameMapper.IsHashSource(trimmedSource)) + { + var quotedValue = trimmedSource.Substring(1, trimmedSource.Length - 2); + contributor.AddHash(quotedValue); + } + } + else if (trimmedSource.Contains(":")) + { + // Check if it's a scheme + if (IsScheme(trimmedSource)) + { + contributor.AddScheme(trimmedSource); + } + else + { + // Treat as host + contributor.AddHost(trimmedSource); + } + } + else + { + // Treat as host (domain without protocol) + contributor.AddHost(trimmedSource); + } + } + + /// + /// Checks if a string represents a scheme. + /// + private static bool IsScheme(string source) + { + string[] knownSchemes = { "http:", "https:", "data:", "blob:", "filesystem:", "wss:", "ws:" }; + return knownSchemes.Contains(source.ToLowerInvariant()); + } + + /// + /// Parses a single directive and applies it to the policy. + /// + private void ParseDirective(string directive) + { + var parts = directive.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + { + return; + } + + var directiveName = parts[0].ToLowerInvariant(); + var sources = parts.Skip(1).ToArray(); + + // Try to get the directive type from the name + if (!CspDirectiveNameMapper.TryGetDirectiveType(directiveName, out var directiveType)) + { + // Unknown directive - ignore for now + throw new ($"Unknown directive: {directiveName}"); + } + + this.ApplyDirectiveToPolicy(directiveType, sources); + } + + /// + /// Parses a reporting endpoints directive string into the provided ContentSecurityPolicy object. + /// + /// The reporting endpoints directive string to parse. + private void ParseReportingEndpointsDirective(string directive) + { + if (string.IsNullOrWhiteSpace(directive)) + { + return; + } + + // Split directive into name=value pairs + var parts = directive.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) + { + throw new ArgumentException("Invalid reporting endpoint format. Expected format: name=\"value\""); + } + + var name = parts[0].Trim(); + var value = parts[1].Trim().Trim('"'); + + // Add the reporting endpoint to the policy + this.policy.AddReportEndpoint(name, value); + } + + /// + /// Applies a parsed directive to the policy object. + /// + private void ApplyDirectiveToPolicy(CspDirectiveType directiveType, string[] sources) + { + switch (directiveType) + { + case CspDirectiveType.SandboxDirective: + this.policy.AddSandbox(string.Join(" ", sources)); + break; + + case CspDirectiveType.PluginTypes: + foreach (var source in sources) + { + this.policy.AddPluginTypes(source); + } + + break; + + case CspDirectiveType.UpgradeInsecureRequests: + this.policy.UpgradeInsecureRequests(); + break; + + case CspDirectiveType.ReportUri: + foreach (var source in sources) + { + this.policy.AddReportEndpoint("default", source); + } + + break; + + case CspDirectiveType.ReportTo: + foreach (var source in sources) + { + this.policy.AddReportTo(source); + } + + break; + + // Source-based directives + default: + var contributor = this.GetSourceContributor(directiveType); + if (contributor != null) + { + foreach (var source in sources) + { + ParseAndAddSource(contributor, source); + } + } + + break; + } + } + + /// + /// Gets the appropriate source contributor for a directive type. + /// + private SourceCspContributor GetSourceContributor(CspDirectiveType directiveType) + { + return directiveType switch + { + CspDirectiveType.DefaultSrc => this.policy.DefaultSource, + CspDirectiveType.ScriptSrc => this.policy.ScriptSource, + CspDirectiveType.StyleSrc => this.policy.StyleSource, + CspDirectiveType.ImgSrc => this.policy.ImgSource, + CspDirectiveType.ConnectSrc => this.policy.ConnectSource, + CspDirectiveType.FontSrc => this.policy.FontSource, + CspDirectiveType.ObjectSrc => this.policy.ObjectSource, + CspDirectiveType.MediaSrc => this.policy.MediaSource, + CspDirectiveType.FrameSrc => this.policy.FrameSource, + CspDirectiveType.FrameAncestors => this.policy.FrameAncestors, + CspDirectiveType.FormAction => this.policy.FormAction, + CspDirectiveType.BaseUri => this.policy.BaseUriSource, + _ => null + }; + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspContributor.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspContributor.cs new file mode 100644 index 00000000000..27279e58df3 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspContributor.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Manages Content Security Policy contributors for a specific directive. + /// + public class CspContributor + { + /// + /// Initializes a new instance of the class. + /// + /// The directive to create the contributor for. + public CspContributor(string directive) + { + this.Directive = directive ?? throw new ArgumentNullException(nameof(directive)); + } + + /// + /// Gets name of the directive (e.g., 'script-src', 'style-src'). + /// + public string Directive { get; } + + /// + /// Gets collection of sources for this directive. + /// + private List Sources { get; } = new List(); + + /// + /// Adds a source to the directive. + /// + /// The source to add. + public void AddSource(CspSource source) + { + if (!this.Sources.Any(s => s.Type == source.Type && s.Value == source.Value)) + { + this.Sources.Add(source); + } + } + + /// + /// Removes a source from the directive. + /// + /// The source to remove. + public void RemoveSource(CspSource source) + { + this.Sources.RemoveAll(s => s.Type == source.Type && s.Value == source.Value); + } + + /// + /// Generates the complete directive string. + /// + /// The directive string. + public string GenerateDirective() + { + if (!this.Sources.Any()) + { + return string.Empty; + } + + return $"{this.Directive} {string.Join(" ", this.Sources.Select(s => s.ToString()))}"; + } + + /// + /// Gets all sources of a specific type. + /// + /// The type of sources to get. + /// The sources of the specified type. + public IEnumerable GetSourcesByType(CspSourceType type) + { + return this.Sources.Where(s => s.Type == type); + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveNameMapper.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveNameMapper.cs new file mode 100644 index 00000000000..2f45a6be448 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveNameMapper.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + + /// + /// Utility class for converting directive types to their string representations. + /// + public static class CspDirectiveNameMapper + { + /// + /// Gets the directive name string. + /// + /// The directive type to get the name for. + /// The directive name string. + public static string GetDirectiveName(CspDirectiveType directiveType) + { + return directiveType switch + { + CspDirectiveType.DefaultSrc => "default-src", + CspDirectiveType.ScriptSrc => "script-src", + CspDirectiveType.StyleSrc => "style-src", + CspDirectiveType.ImgSrc => "img-src", + CspDirectiveType.ConnectSrc => "connect-src", + CspDirectiveType.FontSrc => "font-src", + CspDirectiveType.ObjectSrc => "object-src", + CspDirectiveType.MediaSrc => "media-src", + CspDirectiveType.FrameSrc => "frame-src", + CspDirectiveType.BaseUri => "base-uri", + CspDirectiveType.PluginTypes => "plugin-types", + CspDirectiveType.SandboxDirective => "sandbox", + CspDirectiveType.FormAction => "form-action", + CspDirectiveType.FrameAncestors => "frame-ancestors", + CspDirectiveType.ReportUri => "report-uri", + CspDirectiveType.ReportTo => "report-to", + CspDirectiveType.UpgradeInsecureRequests => "upgrade-insecure-requests", + _ => throw new ArgumentException("Unknown directive type") + }; + } + + /// + /// Gets the directive type from a directive name string. + /// + /// The directive name to get the type for. + /// The directive type. + /// Thrown when the directive name is unknown. + public static CspDirectiveType GetDirectiveType(string directiveName) + { + if (string.IsNullOrWhiteSpace(directiveName)) + { + throw new ArgumentException("Directive name cannot be null or empty", nameof(directiveName)); + } + + return directiveName.ToLowerInvariant() switch + { + "default-src" => CspDirectiveType.DefaultSrc, + "script-src" => CspDirectiveType.ScriptSrc, + "style-src" => CspDirectiveType.StyleSrc, + "img-src" => CspDirectiveType.ImgSrc, + "connect-src" => CspDirectiveType.ConnectSrc, + "font-src" => CspDirectiveType.FontSrc, + "object-src" => CspDirectiveType.ObjectSrc, + "media-src" => CspDirectiveType.MediaSrc, + "frame-src" => CspDirectiveType.FrameSrc, + "base-uri" => CspDirectiveType.BaseUri, + "plugin-types" => CspDirectiveType.PluginTypes, + "sandbox" => CspDirectiveType.SandboxDirective, + "form-action" => CspDirectiveType.FormAction, + "frame-ancestors" => CspDirectiveType.FrameAncestors, + "report-uri" => CspDirectiveType.ReportUri, + "report-to" => CspDirectiveType.ReportTo, + "upgrade-insecure-requests" => CspDirectiveType.UpgradeInsecureRequests, + _ => throw new ArgumentException($"Unknown directive name: {directiveName}") + }; + } + + /// + /// Tries to get the directive type from a directive name string. + /// + /// The directive name to get the type for. + /// The directive type, or default if parsing failed. + /// True if parsing was successful, false otherwise. + public static bool TryGetDirectiveType(string directiveName, out CspDirectiveType directiveType) + { + directiveType = default; + + try + { + directiveType = GetDirectiveType(directiveName); + return true; + } + catch + { + return false; + } + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveType.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveType.cs new file mode 100644 index 00000000000..889db63ea4e --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveType.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + /// + /// Represents different types of Content Security Policy directives. + /// + public enum CspDirectiveType + { + /// + /// Directive qui définit la politique par défaut pour les types de ressources non spécifiés. + /// + DefaultSrc, + + /// + /// Directive qui contrôle les sources de scripts autorisées. + /// + ScriptSrc, + + /// + /// Directive qui contrôle les sources de styles autorisées. + /// + StyleSrc, + + /// + /// Directive qui contrôle les sources d'images autorisées. + /// + ImgSrc, + + /// + /// Directive qui contrôle les destinations de connexion autorisées. + /// + ConnectSrc, + + /// + /// Directive qui contrôle les sources de polices autorisées. + /// + FontSrc, + + /// + /// Directive qui contrôle les sources d'objets autorisées. + /// + ObjectSrc, + + /// + /// Directive qui contrôle les sources de médias autorisées. + /// + MediaSrc, + + /// + /// Directive qui contrôle les sources de frames autorisées. + /// + FrameSrc, + + /// + /// Directive qui restreint les URLs pouvant être utilisées dans la base URI du document. + /// + BaseUri, + + /// + /// Directive qui restreint les types de plugins pouvant être chargés. + /// + PluginTypes, + + /// + /// Directive qui active un bac à sable pour la ressource demandée. + /// + SandboxDirective, + + /// + /// Directive qui restreint les URLs pouvant être utilisées comme cible de formulaire. + /// + FormAction, + + /// + /// Directive qui spécifie les parents autorisés à intégrer une page dans un frame. + /// + FrameAncestors, + + /// + /// Directive qui spécifie l'URI où envoyer les rapports de violation. + /// + ReportUri, + + /// + /// Directive qui spécifie où envoyer les rapports de violation au format JSON. + /// + ReportTo, + + /// + /// Directive qui spécifie UpgradeInsecureRequests. + /// + UpgradeInsecureRequests, +} +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspParsingExample.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspParsingExample.cs new file mode 100644 index 00000000000..b10d2311eb4 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspParsingExample.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + + /// + /// Example class demonstrating how to parse Content Security Policy headers. + /// + public static class CspParsingExample + { + /// + /// Demonstrates how to parse a CSP header string. + /// + public static void ParseExample() + { + // Example CSP header string + var cspHeader = "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com 'nonce-abc123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self' https://fonts.googleapis.com; frame-ancestors 'none'; report-uri /csp-report"; + var policy = new ContentSecurityPolicy(); + try + { + // Parse the CSP header + policy.AddHeader(cspHeader); + + // Access parsed directives + Console.WriteLine("Parsed CSP Policy:"); + Console.WriteLine($"Generated Policy: {policy.GeneratePolicy()}"); + Console.WriteLine($"Nonce: {policy.Nonce}"); + + // You can now modify the parsed policy + policy.ScriptSource.AddHost("newcdn.example.com"); + policy.StyleSource.AddHash("sha256-abc123def456"); + + Console.WriteLine($"Modified Policy: {policy.GeneratePolicy()}"); + } + catch (ArgumentException ex) + { + Console.WriteLine($"Failed to parse CSP header: {ex.Message}"); + } + + // Example using TryParse + var invalidCspHeader = "invalid-directive something"; + try + { + policy.AddHeader(invalidCspHeader); + Console.WriteLine("Successfully parsed invalid header"); + } + catch (Exception) + { + Console.WriteLine("Failed to parse invalid header (as expected)"); + } + } + + /// + /// Demonstrates various CSP header formats that can be parsed. + /// + public static void ParseVariousFormats() + { + var examples = new[] + { + // Basic policy + "default-src 'self'", + + // Policy with multiple sources + "script-src 'self' 'unsafe-inline' https://cdn.example.com", + + // Policy with nonce + "script-src 'self' 'nonce-abc123def456'", + + // Policy with hash + "style-src 'self' 'sha256-abc123def456789'", + + // Complex policy + "default-src 'self'; script-src 'self' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' wss:; font-src 'self' https://fonts.googleapis.com; frame-ancestors 'none'; upgrade-insecure-requests; report-uri /csp-report", + + // Policy with sandbox + "sandbox allow-forms allow-scripts; script-src 'self'", + + // Policy with form-action + "form-action 'self' https://secure.example.com", + + // Policy with report-uri + "default-src 'self'; img-src 'self' https://front.satrabel.be https://www.googletagmanager.com https://region1.google-analytics.com; font-src 'self' https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com https://www.googletagmanager.com; frame-ancestors 'self'; frame-src 'self'; form-action 'self'; object-src 'none'; base-uri 'self'; script-src 'nonce-hq9CE6VltPZiiySID0F9914GvPObOnIAN3Qs/0R+AmQ=' 'strict-dynamic'; report-to csp-endpoint; report-uri https://dnncore.satrabel.be/DesktopModules/Csp/Report; connect-src https://www.googletagmanager.com https://region1.google-analytics.com https://www.google-analytics.com; upgrade-insecure-requests", + }; + + foreach (var example in examples) + { + Console.WriteLine($"\nParsing: {example}"); + var policy = new ContentSecurityPolicy(); + try + { + policy.AddHeader(example); + Console.WriteLine($"Success: {policy.GeneratePolicy()}"); + } + catch (Exception) + { + Console.WriteLine("Failed to parse"); + } + } + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspPolicyExample.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspPolicyExample.cs new file mode 100644 index 00000000000..c82f8bdde82 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspPolicyExample.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + + /// + /// Démontre l'utilisation de la Content Security Policy en configurant différentes directives. + /// + public class CspPolicyExample + { + /// + /// Démontre l'utilisation de la Content Security Policy en configurant différentes directives. + /// + public static void Example() + { + // Create a Content Security Policy + var csp = new ContentSecurityPolicy(); + + // Add a source-based contributor for script sources + csp.ScriptSource + .AddSelf() + .AddHost("https://trusted-cdn.com"); + + // Add a document-based contributor for sandbox + csp.AddSandbox("allow-scripts allow-same-origin"); + + // Add a reporting contributor + csp.AddReportEndpoint("name", "https://example.com/csp-report"); + + // Generate the complete policy + string policy = csp.GeneratePolicy(); + + Console.WriteLine(policy); + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSource.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSource.cs new file mode 100644 index 00000000000..93b04cf7565 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSource.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Represents a single source in a Content Security Policy. + /// + public class CspSource + { + /// + /// Initializes a new instance of the class. + /// + /// Type of the source. + /// Value of the source. + public CspSource(CspSourceType type, string value = null) + { + this.Type = type; + this.Value = this.ValidateSource(type, value); + } + + /// + /// Gets type of the CSP source. + /// + public CspSourceType Type { get; } + + /// + /// Gets the actual source value. + /// + public string Value { get; } + + /// + /// Returns the string representation of the source. + /// + /// The string representation of the source. + public override string ToString() => this.Value ?? CspSourceTypeNameMapper.GetSourceTypeName(this.Type); + + /// + /// Validates the source based on its type. + /// + private string ValidateSource(CspSourceType type, string value) + { + switch (type) + { + case CspSourceType.Host: + return this.ValidateHostSource(value); + case CspSourceType.Scheme: + return this.ValidateSchemeSource(value); + case CspSourceType.Nonce: + return this.ValidateNonceSource(value); + case CspSourceType.Hash: + return this.ValidateHashSource(value); + case CspSourceType.Self: + return "'self'"; + case CspSourceType.Inline: + case CspSourceType.Eval: + return "'unsafe-" + type.ToString().ToLowerInvariant() + "'"; + case CspSourceType.None: + return "'none'"; + case CspSourceType.StrictDynamic: + return "'strict-dynamic'"; + default: + throw new ArgumentException("Invalid source type"); + } + } + + /// + /// Validates host source (domain or IP). + /// + private string ValidateHostSource(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Host source cannot be empty"); + } + + // Basic domain validation + var domainRegex = new Regex(@"^(https?://)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(/.*)?$"); + if (!domainRegex.IsMatch(value)) + { + throw new ArgumentException($"Invalid host source: {value}"); + } + + return value.StartsWith("http") ? value : $"https://{value}"; + } + + /// + /// Validates scheme source (protocol). + /// + private string ValidateSchemeSource(string value) + { + string[] validSchemes = { "http:", "https:", "data:", "blob:", "filesystem:", "wss:", "ws:" }; + if (!validSchemes.Contains(value)) + { + throw new ArgumentException($"Invalid scheme: {value}"); + } + + return value; + } + + /// + /// Validates nonce source. + /// + private string ValidateNonceSource(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Nonce cannot be empty"); + } + + // Basic nonce validation - allow any non-empty string for flexibility + // In real-world scenarios, nonces might not always be strict base64 + return $"'nonce-{value}'"; + } + + /// + /// Validates hash source. + /// + private string ValidateHashSource(string value) + { + string[] hashPrefixes = { "sha256-", "sha384-", "sha512-" }; + + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Hash cannot be empty"); + } + + // Check if the value starts with a valid hash prefix + // Allow any string after the prefix for flexibility in parsing scenarios + bool hasValidPrefix = hashPrefixes.Any(prefix => value.StartsWith(prefix)); + + if (!hasValidPrefix) + { + throw new ArgumentException($"Invalid hash format: {value}"); + } + + return $"'{value}'"; + } + + /// + /// Checks if a string is a valid Base64 string. + /// + private bool IsBase64String(string value) + { + try + { + Convert.FromBase64String(value); + return true; + } + catch + { + return false; + } + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceType.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceType.cs new file mode 100644 index 00000000000..496ab88dbb0 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceType.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + /// + /// Represents different types of Content Security Policy source types. + /// + public enum CspSourceType + { + /// + /// Permet de spécifier des domaines spécifiques comme source. + /// + Host, + + /// + /// Permet de spécifier des protocoles (ex: https:, data:) comme source. + /// + Scheme, + + /// + /// Autorise les ressources de la même origine ('self'). + /// + Self, + + /// + /// Autorise l'utilisation de code inline ('unsafe-inline'). + /// + Inline, + + /// + /// Autorise l'utilisation de eval() ('unsafe-eval'). + /// + Eval, + + /// + /// Utilise un nonce cryptographique pour valider les ressources. + /// + Nonce, + + /// + /// Utilise un hash cryptographique pour valider les ressources. + /// + Hash, + + /// + /// N'autorise aucune source ('none'). + /// + None, + + /// + /// Active le mode strict-dynamic pour le chargement des scripts. + /// + StrictDynamic, + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceTypeNameMapper.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceTypeNameMapper.cs new file mode 100644 index 00000000000..60c8f655192 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceTypeNameMapper.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + + /// + /// Utility class for converting source types to their string representations. + /// + public static class CspSourceTypeNameMapper + { + /// + /// Gets the source type name string. + /// + /// The source type to get the name for. + /// The source type name string. + public static string GetSourceTypeName(CspSourceType sourceType) + { + return sourceType switch + { + CspSourceType.Host => "host", + CspSourceType.Scheme => "scheme", + CspSourceType.Self => "'self'", + CspSourceType.Inline => "'unsafe-inline'", + CspSourceType.Eval => "'unsafe-eval'", + CspSourceType.Nonce => "nonce", + CspSourceType.Hash => "hash", + CspSourceType.None => "'none'", + CspSourceType.StrictDynamic => "'strict-dynamic'", + _ => throw new ArgumentException("Unknown source type") + }; + } + + /// + /// Gets the source type from a source name string. + /// + /// The source name to get the type for. + /// The source type. + /// Thrown when the source name is unknown. + public static CspSourceType GetSourceType(string sourceName) + { + if (string.IsNullOrWhiteSpace(sourceName)) + { + throw new ArgumentException("Source name cannot be null or empty", nameof(sourceName)); + } + + return sourceName.ToLowerInvariant() switch + { + "'self'" => CspSourceType.Self, + "'unsafe-inline'" => CspSourceType.Inline, + "'unsafe-eval'" => CspSourceType.Eval, + "'none'" => CspSourceType.None, + "'strict-dynamic'" => CspSourceType.StrictDynamic, + _ => throw new ArgumentException($"Unknown source name: {sourceName}") + }; + } + + /// + /// Tries to get the source type from a source name string. + /// + /// The source name to get the type for. + /// The source type, or default if parsing failed. + /// True if parsing was successful, false otherwise. + public static bool TryGetSourceType(string sourceName, out CspSourceType sourceType) + { + sourceType = default; + + try + { + sourceType = GetSourceType(sourceName); + return true; + } + catch + { + return false; + } + } + + /// + /// Checks if a source string represents a quoted keyword. + /// + /// The source string to check. + /// True if the source is a quoted keyword, false otherwise. + public static bool IsQuotedKeyword(string source) + { + if (string.IsNullOrWhiteSpace(source)) + { + return false; + } + + return source.StartsWith("'") && source.EndsWith("'"); + } + + /// + /// Checks if a source string represents a nonce value. + /// + /// The source string to check. + /// True if the source is a nonce value, false otherwise. + public static bool IsNonceSource(string source) + { + if (string.IsNullOrWhiteSpace(source)) + { + return false; + } + + return source.StartsWith("'nonce-") && source.EndsWith("'"); + } + + /// + /// Checks if a source string represents a hash value. + /// + /// The source string to check. + /// True if the source is a hash value, false otherwise. + public static bool IsHashSource(string source) + { + if (string.IsNullOrWhiteSpace(source)) + { + return false; + } + + return source.StartsWith("'") && source.EndsWith("'") && + (source.Contains("sha256-") || source.Contains("sha384-") || source.Contains("sha512-")); + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/DocumentCspContributor.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/DocumentCspContributor.cs new file mode 100644 index 00000000000..3457dfe3ce9 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/DocumentCspContributor.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + using System.Linq; + + /// + /// Contributor for document-level directives. + /// + public class DocumentCspContributor : BaseCspContributor + { + /// + /// Initializes a new instance of the class. + /// + /// The directive type to create the contributor for. + /// The value of the directive. + public DocumentCspContributor(CspDirectiveType directiveType, string value) + { + this.DirectiveType = directiveType; + this.SetDirectiveValue(value); + } + + /// + /// Gets value of the document directive. + /// + public string DirectiveValue { get; private set; } + + /// + /// Sets the directive value with validation. + /// + /// The value to set for the directive. + public void SetDirectiveValue(string value) + { + this.ValidateDirectiveValue(this.DirectiveType, value); + this.DirectiveValue = value; + } + + /// + /// Generates the directive string. + /// + /// The directive string. + public override string GenerateDirective() + { + if (this.DirectiveType == CspDirectiveType.UpgradeInsecureRequests) + { + return $"{CspDirectiveNameMapper.GetDirectiveName(this.DirectiveType)}"; + } + + if (string.IsNullOrWhiteSpace(this.DirectiveValue)) + { + return string.Empty; + } + + return $"{CspDirectiveNameMapper.GetDirectiveName(this.DirectiveType)} {this.DirectiveValue}"; + } + + /// + /// Validates directive value based on directive type. + /// + private void ValidateDirectiveValue(CspDirectiveType type, string value) + { + switch (type) + { + case CspDirectiveType.PluginTypes: + this.ValidatePluginTypes(value); + break; + case CspDirectiveType.SandboxDirective: + this.ValidateSandboxDirective(value); + break; + + // Add more specific validations as needed + } + } + + /// + /// Validates plugin types. + /// + private void ValidatePluginTypes(string value) + { + string[] validPluginTypes = { "application/pdf", "image/svg+xml" }; + var types = value.Split(' '); + + if (types.Any(t => !validPluginTypes.Contains(t))) + { + throw new ArgumentException("Invalid plugin type"); + } + } + + /// + /// Validates sandbox directive values. + /// + private void ValidateSandboxDirective(string value) + { + string[] validSandboxValues = + { + "allow-forms", + "allow-scripts", + "allow-same-origin", + "allow-top-navigation", + "allow-popups", + }; + + var values = value.Split(' '); + + if (values.Any(v => !validSandboxValues.Contains(v))) + { + throw new ArgumentException("Invalid sandbox directive value"); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/DotNetNuke.ContentSecurityPolicy.csproj b/DNN Platform/DotNetNuke.ContentSecurityPolicy/DotNetNuke.ContentSecurityPolicy.csproj new file mode 100644 index 00000000000..738aa41a77d --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/DotNetNuke.ContentSecurityPolicy.csproj @@ -0,0 +1,45 @@ + + + + netstandard2.0 + false + true + $(MSBuildProjectDirectory)\..\.. + bin/$(Configuration)/$(TargetFramework)/DotNetNuke.ContentSecurityPolicy.xml + + + true + latest + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + SolutionInfo.cs + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/IContentSecurityPolicy.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/IContentSecurityPolicy.cs new file mode 100644 index 00000000000..4f507829e81 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/IContentSecurityPolicy.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.ContentSecurityPolicy +{ + using System; + + /// + /// Interface définissant les opérations de gestion de la Content Security Policy. + /// + public interface IContentSecurityPolicy + { + /// + /// Gets a cryptographically secure nonce value for the CSP policy. + /// + string Nonce { get; } + + /// + /// Gets the default source contributor. + /// + SourceCspContributor DefaultSource { get; } + + /// + /// Gets the script source contributor. + /// + SourceCspContributor ScriptSource { get; } + + /// + /// Gets the style source contributor. + /// + SourceCspContributor StyleSource { get; } + + /// + /// Gets the image source contributor. + /// + SourceCspContributor ImgSource { get; } + + /// + /// Gets the connect source contributor. + /// + SourceCspContributor ConnectSource { get; } + + /// + /// Gets the font source contributor. + /// + SourceCspContributor FontSource { get; } + + /// + /// Gets the object source contributor. + /// + SourceCspContributor ObjectSource { get; } + + /// + /// Gets the media source contributor. + /// + SourceCspContributor MediaSource { get; } + + /// + /// Gets the frame source contributor. + /// + SourceCspContributor FrameSource { get; } + + /// + /// Gets the frame ancestors contributor. + /// + SourceCspContributor FrameAncestors { get; } + + /// + /// Gets the Form action source contributor. + /// + SourceCspContributor FormAction { get; } + + /// + /// Gets the base URI source contributor. + /// + SourceCspContributor BaseUriSource { get; } + + /// + /// Supprimer une source de script à la politique. + /// + /// Le type de source CSP à supprimer. + void RemoveScriptSources(CspSourceType cspSourceType); + + /// + /// Ajoute des types de plugins à la politique. + /// + /// Le type de plugin à autoriser. + void AddPluginTypes(string value); + + /// + /// Ajoute une directive sandbox à la politique. + /// + /// Les options de la directive sandbox. + void AddSandbox(string value); + + /// + /// Ajoute une action de formulaire à la politique. + /// + /// Le type de source CSP à ajouter. + /// L'URL autorisée pour la soumission du formulaire. + void AddFormAction(CspSourceType sourceType, string value); + + /// + /// Ajoute des ancêtres de frame à la politique. + /// + /// Le type de source CSP à ajouter. + /// L'URL autorisée comme ancêtre de frame. + void AddFrameAncestors(CspSourceType sourceType, string value); + + /// + /// Ajoute une URI de rapport à la politique. + /// + /// Le nom où les rapports de violation seront envoyés. + /// L'URI où les rapports de violation seront envoyés. + public void AddReportEndpoint(string name, string value); + + /// + /// Ajoute une destination de rapport à la politique. + /// + /// L'endpoint où envoyer les rapports. + void AddReportTo(string value); + + /// + /// Parses a CSP header string into a ContentSecurityPolicy object. + /// + /// The CSP header string to parse. + /// A ContentSecurityPolicy object representing the parsed header. + /// Thrown when the CSP header is invalid or cannot be parsed. + IContentSecurityPolicy AddHeader(string cspHeader); + + /// + /// Ajoute une directive de rapport à la politique. + /// + /// La directive de rapport à ajouter. + /// A ContentSecurityPolicy object representing the parsed header. + IContentSecurityPolicy AddReportEndpointHeader(string header); + + /// + /// Génère la politique de sécurité complète. + /// + /// La politique de sécurité complète sous forme de chaîne. + string GeneratePolicy(); + + /// + /// Génère la politique de sécurité complète. + /// + /// Reporting Endpoints sous forme de chaîne. + string GenerateReportingEndpoints(); + + /// + /// Upgrade Insecure Requests. + /// + void UpgradeInsecureRequests(); + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/README.md b/DNN Platform/DotNetNuke.ContentSecurityPolicy/README.md new file mode 100644 index 00000000000..33845255c6a --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/README.md @@ -0,0 +1,119 @@ +# DotNetNuke.ContentSecurityPolicy + +The `DotNetNuke.ContentSecurityPolicy` library provides a fluent API for building and emitting Content Security Policy (CSP) headers in DNN. The `IContentSecurityPolicy` interface is the main entry point to compose directives, manage sources, configure reporting, and generate final header strings. + +## Interface: `IContentSecurityPolicy` +Namespace: `DotNetNuke.ContentSecurityPolicy` + +### Properties +- **Nonce**: Cryptographically secure nonce value to use with inline script/style tags. +- **DefaultSource**: `SourceCspContributor` for `default-src`. +- **ScriptSource**: `SourceCspContributor` for `script-src`. +- **StyleSource**: `SourceCspContributor` for `style-src`. +- **ImgSource**: `SourceCspContributor` for `img-src`. +- **ConnectSource**: `SourceCspContributor` for `connect-src`. +- **FontSource**: `SourceCspContributor` for `font-src`. +- **ObjectSource**: `SourceCspContributor` for `object-src`. +- **MediaSource**: `SourceCspContributor` for `media-src`. +- **FrameSource**: `SourceCspContributor` for `frame-src`. +- **FrameAncestors**: `SourceCspContributor` for `frame-ancestors`. +- **FormAction**: `SourceCspContributor` for `form-action`. +- **BaseUriSource**: `SourceCspContributor` for `base-uri`. + +### Methods +- **RemoveScriptSources(CspSourceType cspSourceType)**: Remove script sources of the specified type (e.g., `Inline`, `Self`, `Nonce`). +- **AddPluginTypes(string value)**: Add values for `plugin-types` (e.g., `application/pdf`). +- **AddSandboxDirective(string value)**: Add `sandbox` options (e.g., `allow-scripts allow-same-origin`). +- **AddFormAction(CspSourceType sourceType, string value)**: Add a `form-action` source. +- **AddFrameAncestors(CspSourceType sourceType, string value)**: Add a `frame-ancestors` source. +- **AddReportEndpoint(string name, string value)**: Add a named reporting endpoint. +- **AddReportTo(string value)**: Add a `report-to` group name to the policy. +- **AddHeaders(string cspHeader)**: Parse and merge a CSP header string; returns the same `IContentSecurityPolicy` for chaining. +- **GeneratePolicy()**: Build the `Content-Security-Policy` header value. +- **GenerateReportingEndpoints()**: Build the reporting header value(s). +- **UpgradeInsecureRequests()**: Add the `upgrade-insecure-requests` directive. + +## Working with sources +Directive properties expose a `SourceCspContributor`, which supports adding/removing sources such as: +- `AddSelf()` → `'self'` +- `AddNone()` → `'none'` +- `AddInline()` → `'unsafe-inline'` +- `AddEval()` → `'unsafe-eval'` +- `AddStrictDynamic()` → `'strict-dynamic'` +- `AddNonce(string)` → `'nonce-'` +- `AddHash(string)` → `'sha256-...'`, `'sha384-...'`, `'sha512-...'` +- `AddHost(string)` → `example.com`, `https://cdn.example.com` +- `AddScheme(string)` → `https:`, `data:`, `blob:` +- `RemoveSources(CspSourceType)` to remove by type + +See: `CspSourceType.cs`, `CspSource.cs`, `SourceCspContributor.cs`. + +## Usage examples + +### Configure a baseline policy with a nonce +```csharp +using DotNetNuke.ContentSecurityPolicy; + +public class CspExample +{ + private readonly IContentSecurityPolicy _csp; + + public CspExample(IContentSecurityPolicy csp) + { + _csp = csp; + } + + public void Configure() + { + // Default baseline + _csp.DefaultSource.AddSelf(); + _csp.ScriptSource.AddSelf().AddNonce(_csp.Nonce); + _csp.StyleSource.AddSelf().AddNonce(_csp.Nonce); + _csp.ImgSource.AddSelf().AddScheme("data:"); + + // Lock down frames and forms + _csp.FrameAncestors.AddNone(); + _csp.FormAction.AddSelf(); + + // Reporting + _csp.AddReportEndpoint("csp-endpoint", "/api/csp/report"); + _csp.AddReportTo("csp-endpoint"); + + // Optionally upgrade insecure requests + _csp.UpgradeInsecureRequests(); + + // Generate header values + var cspHeader = _csp.GeneratePolicy(); + var reportingHeader = _csp.GenerateReportingEndpoints(); + // Emit headers via your pipeline/middleware/module + } +} +``` + +### Parse and merge an existing CSP header +```csharp +_csp.AddHeaders("default-src 'self'; img-src 'self' data:") + .ScriptSource.AddNonce(_csp.Nonce); + +var headerValue = _csp.GeneratePolicy(); +``` + +### Remove an unsafe source +```csharp +_csp.RemoveScriptSources(CspSourceType.Inline); +``` + +## Notes +- Nonce: use `Nonce` in your inline tags: `