Skip to content

Conversation

@sachatrauwaen
Copy link
Contributor

@sachatrauwaen sachatrauwaen commented Oct 3, 2025

Summary

Create a possibility to generate Content security policy (csp) headers .

Content Security Policy is a crucial security standard that helps protect your web applications from various types of attacks, including Cross-Site Scripting (XSS), clickjacking, and other code injection attacks. It works by allowing you to specify which resources (scripts, styles, images, etc.) your browser should be allowed to load.
more info about Content security policy

Description of solution

The intend is to manage CSP for WebForms and MVC pipeline.

  • Webforms can not be very struct in the policy that can be used. Here script-src 'unsafe-inline' and 'unsafe-eval' will be automatically added to csp. It is not added in the settings because it's specific for webforms.

  • In the future MVC pipeline can be very strict. Here no js evaluation will be used and all inline javascript will be marked with a nonce.

This is the default csp for the setting

default-src 'self'; script-src 'self' 'report-sample'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; frame-src 'self'; connect-src 'self';

  • It dous not accept external resources.
  • It accept inline css : actually used by the default page of dnn and generated by ckeditor with default configuration. But it is not recomended. ckeditor can be configurated to not use inline css.
  • It accept image src starting with data: bacause it is used by default dnn page. But it is not recomended.

3 http headers will be managed : Content-Security-Policy, Content-Security-Policy-Report-Only and Reporting-Endpoints

Persona bar - Security settings

image

Implementation

It commes in a new project for csp management and a test project.

The IContentSecurityPolicy service that can be used with DI had all the stuff for skin and module developers to contribute to the policy.

For webforms skin developers actually the way to contribute is

<script runat="server">
    protected void Page_Init()
    {
        var defaultPage = (DotNetNuke.Framework.DefaultPage)this.Page;
        defaultPage.AddCsp("script-src https://ajax.aspnetcdn.com");
    }
</script>

Details of DotNetNuke.ContentSecurityPolicy library

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-<value>'
  • 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

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();
        _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();

    }
}

Parse and merge an existing CSP header

_csp.AddHeaders("default-src 'self'; img-src 'self' data:")
    .ScriptSource.AddNonce(_csp.Nonce);

var headerValue = _csp.GeneratePolicy();

Remove an unsafe source

_csp.RemoveScriptSources(CspSourceType.Inline);

Notes

  • Nonce: use Nonce in your inline tags: <script nonce="{policy.Nonce}">.
  • Reporting: ensure your endpoint exists to accept violation reports.
  • Parsing: AddHeaders is useful to import settings from configuration and extend them programmatically.
  • This is not only about js and css but also about images, iframes and more (parts of the content).

Update 22 october 2025

There are 2 point of views :

  1. The site settings are there for the site admin to enforce some policies. In this case, the policy need to include all requirements (from skins, modules, skin object, content, ...) and all pages (not only dnn pages but also edit urls need to be included in this policies to get them work properly). Actually this can already done with a the web.config setting. Or you need different policies for different use cases/ user profils (Anonymous, loged in, page admin, module editor, host, ...).

  2. (The point of view of this PR) The site settings are there for defining a starting policy where skins, modules, ... can add dynamically there policy. So the policy will be adjusted automatically to make the site work.

What is sure is that modules and skins will paticipate in the ability to make the policies strict.
Knowing witch policies are required by a skin or modules is key even what kind of content is permited, if you want to be strict.

Know that browsers include also a way to report csp policy violations to the browser console and to a endpoint.

Fixes #6720

@mitchelsellers
Copy link
Contributor

With this, how exactly does the configured process in the persona bar work with the fact that anyone could manipulate this via other methods in the skin, module, etc?

Is the portal done first and then changes applied?

How is the users input validated in the personarbar validated? is it serialized to an object with detailed validation errors?

@sachatrauwaen sachatrauwaen marked this pull request as ready for review October 3, 2025 15:48
@sachatrauwaen
Copy link
Contributor Author

sachatrauwaen commented Oct 3, 2025

With this, how exactly does the configured process in the persona bar work with the fact that anyone could manipulate this via other methods in the skin, module, etc?
Is the portal done first and then changes applied?

Yes, the portal settings are applied first in the OnInit of the page (default.aspx).
Then skin and modules can add there policy. Normally skins and modules will only add addional policy.
And finally in the OnPreRender of the page the full policy string is generated and added to the http response header.

How is the users input validated in the personarbar validated? is it serialized to an object with detailed validation errors?

The library containt a policy parser. But actully no validation error is showed to the user that enter the settings.
I will look to add this.

@dnnsoftware dnnsoftware deleted a comment from mitchelsellers Oct 3, 2025
@dnnsoftware dnnsoftware deleted a comment from mitchelsellers Oct 3, 2025
@donker
Copy link
Contributor

donker commented Nov 7, 2025

@sachatrauwaen: can you add a switch for the admin that governs whether a module can enlarge the CSP scope or not? That way the admin decides what is allowed and knows what is going to happen.

@sachatrauwaen
Copy link
Contributor Author

sachatrauwaen commented Nov 12, 2025

@sachatrauwaen: can you add a switch for the admin that governs whether a module can enlarge the CSP scope or not? That way the admin decides what is allowed and knows what is going to happen.

In reality there 2 ways to add policies to CSP : 1) site settings 2) api (used by the core, modules, skins, editor providers and potentialy all other extensions).

The site settings where actually for define the default strict policy, policy for content (images, iframe, css) and policy for modules that not automatically add there policy.

When we want to add a "switch for the admin that governs whether a module can enlarge the CSP scope or not" it is for all policies added by api not only modules. So we add only the policy from the site settings.

# Conflicts:
#	DNN Platform/Website/Default.aspx.cs
@sachatrauwaen
Copy link
Contributor Author

New Sitting is added

image

Comment on lines 187 to 190
public void AddCsp(string policy)
{
this.ContentSecurityPolicy.AddHeader(policy);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this method need to be public?

Comment on lines 45 to 48
/// <summary>
/// Ajoute une source 'self' qui autorise les ressources de la même origine.
/// </summary>
/// <returns>L'instance courante pour chaîner les méthodes.</returns>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This documentation appears to be in french, we usually do it in english for a wider audience of developers

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for multiple others in this file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

if (this.nonce == null)
{
var nonceBytes = new byte[32];
var generator = System.Security.Cryptography.RandomNumberGenerator.Create();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we create a single instance of the RandomNumberGenerator at the class level so we dont do a new one each time a resource is added. This service is registered as scoped so that way we could reuse that existing instance if many resources get added in the same request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we create a single instance of the RandomNumberGenerator at the class level so we dont do a new one each time a resource is added.

Because it is scoped, a new one created for each request only (not for each resource added) and if Nonce is used.

And because it implements IDisposable (as Mich say) if it will be create at class level, it will be never disposed.

public enum CspDirectiveType
{
/// <summary>
/// Directive qui définit la politique par défaut pour les types de ressources non spécifiés.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class also has most of its documentation in french

/// <summary>
/// Example class demonstrating how to parse Content Security Policy headers.
/// </summary>
public static class CspParsingExample
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move usage examples to the tests project maybe, or maybe tests are good enough examples?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i remove the examples

/// <summary>
/// Démontre l'utilisation de la Content Security Policy en configurant différentes directives.
/// </summary>
public class CspPolicyExample
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, maybe tests could replace examples here...

if (this.nonce == null)
{
var nonceBytes = new byte[32];
var generator = System.Security.Cryptography.RandomNumberGenerator.Create();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, RandomNumberGenerator is a class that implements IDisposable, it should at minimum be within a using to prevent resource leaking

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i add using

}

// Basic domain validation
var domainRegex = new Regex(@"^(https?://)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(/.*)?$");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am no expert in CSP but I think this regex might be wrong and maybe not optimal for performance too. I believe HostSource should support extensionless domains like localhost and wildcards like *.somecdn.com or simply *, domains with long TLDs like something.technology, IP and port, IPV6, etc. So it that should be a supported scenario I would love that we have some tests for a list of those that should be supported.

Maybe we could allow some of CSP specific scenarios like "*" and similar, then run Uri.TryCreate(value) to validate the more normal ones and if it still does not pass, we can do IPAddress.TryParse(value)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, this is in a hot path for execution, compiling this regex at minimum should be done for performance, and limit processing time as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I change it.
But I was thinking that all the regexp validation is maybe overkill. And because it is evaluated on each request , its maybe better to remove it.
What do you think ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think given it is executed on EVERY request, I'd be very inclined to NOT do this validation and test that it was correct, or find a way to validate at a different stage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i remove the syntax checks from all request. They only be done on site settings save.

/// </summary>
private void ValidatePluginTypes(string value)
{
string[] validPluginTypes = { "application/pdf", "image/svg+xml" };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct to hardcode only those mime-types, I am not sure but should this allow other types like word/excel, etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modified

IContentSecurityPolicy AddHeader(string cspHeader);

/// <summary>
/// Ajoute une directive de rapport à la politique.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file also has some french documentation

}

// URL validation regex
var urlRegex = new Regex(@"^(https?://)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(:\d+)?(/.*)?$");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could use Uri.TryParse here too to validate without a complex regex?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, this is a validation that is shared across multiple sections, it would be good to standardize this validaton if we can should it need to change.

}

// URL validation regex
var urlRegex = new Regex(@"^(https?://)([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(:\d+)?(/.*)?$");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could use URI.TryParse also?

@@ -0,0 +1,16 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

// set global page settings
this.InitializePage();

if (!this.PortalSettings.CspHeaderFixed &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code for when CspHeaderFixed is true is pretty far from another block of code that handles when it is false. In am not sure if it needs to be that way because there is an important order or operations, but if not, can we possibly bring those together in a private method and improve the readability of this file?

private string nonce;

/// <summary>Initializes a new instance of the <see cref="ContentSecurityPolicy"/> class.</summary>
public ContentSecurityPolicy()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we don't need to do anything special here with the constructor, this should be omitted

/// </summary>
private static bool IsScheme(string source)
{
string[] knownSchemes = { "http:", "https:", "data:", "blob:", "filesystem:", "wss:", "ws:" };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency in naming this should be var knownSchemes

}

// Basic domain validation
var domainRegex = new Regex(@"^(https?://)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(/.*)?$");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, this is in a hot path for execution, compiling this regex at minimum should be done for performance, and limit processing time as well

/// </summary>
private string ValidateSchemeSource(string value)
{
string[] validSchemes = { "http:", "https:", "data:", "blob:", "filesystem:", "wss:", "ws:" };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For coding consistency this should be var

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment for other items in this file

/// Sets the directive value with validation.
/// </summary>
/// <param name="value">The value to set for the directive.</param>
public void SetDirectiveValue(string value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was a separate method implement for this, rather than just doing this as a setter?

}

// URL validation regex
var urlRegex = new Regex(@"^(https?://)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(:\d+)?(/.*)?$");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, this is a validation that is shared across multiple sections, it would be good to standardize this validaton if we can should it need to change.

/// <summary>Content Security Header is not added.</summary>
Off = 0,

/// <summary>Content Security Header is not added in Report Only.</summary>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this summary is incorrect and it should be "Content Security Header is added in Report Only mode."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

enhance mine type validation
replace french doc with english
replace regexp for url validation with Uri.TryCreate
cleanup
syntax check is only done when saving site settings
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status
Status: No status

Development

Successfully merging this pull request may close these issues.

[Enhancement]: Content security policy headers

4 participants