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

Middleware and TagHelpers for CSP support in ASP.NET (#1) #298

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions CSP/CSP.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"solution": {
"path": "..\\..\\..\\AspNetCore.sln",
"projects": [
"src\\Middleware\\CSP\\src\\Microsoft.AspNetCore.Csp.csproj",
"src\\Middleware\\CSP\\test\\UnitTests\\Microsoft.AspNetCore.Csp.Test.csproj",
"src\\Middleware\\CSP\\test\\testassets\\CspMiddlewareWebSite.csproj",
]
}
}
38 changes: 38 additions & 0 deletions CSP/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# CSP

## Description

This directory contains .NET Core middleware for Content Security Policy (CSP). CSP is a very popular security mitigation against XSS and other injection vulnerabilities. CSP comes in many flavours, but we've chosen to add support for the most robust of them: nonce-based, strict-dynamic CSP.

Design document: [Implementing CSP Support in .NET Core](https://docs.google.com/document/d/13NPKn65Wf1PdIwNL7H0cxhwmp2r8ZTe6vizXzO2HqY4/edit#)
There was a previous discussion about CSP in .NET [here](https://github.com/dotnet/aspnetcore/issues/6001), that we have considered for our design.

## Contributions
This directory includes the following changes:

* Allow configuration of whether CSP enabled in reporting or enforcement modes.
* Allows configuration of a report URI, for violation reports sent by the browser.
* CSP middleware generates a nonce-based, strict-dynamic policy.
* Middleware adds the policy to HTTP responses according to the configuration.
* Custom <script> TagHelper to set nonce attribute on script blocks automatically.
* Provides a default implementation of a CSP violation report collection endpoint.
* Example app that uses our CSP middleware and corresponding basic unit tests.

## Usage:

```
// CSP configuration. Must come first because other middleware might skip any following middleware.

app.UseCsp(policyBuilder =>
policyBuilder.WithCspMode(CspMode.ENFORCING)

.WithReportingUri("/csp"));
```
You can find the sample app under `./test/testassets/CspApplication/` directory.

# Point of contact
* Barry Dorrans - [email protected]

## Authors
* Co-authored-by: Aaron Shim - [email protected]
* Co-authored-by: Santiago Diaz - [email protected]
72 changes: 72 additions & 0 deletions CSP/src/ContentSecurityPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Text;

namespace Microsoft.AspNetCore.Csp
{
/// <summary>
/// A greedy Content Security Policy generator
/// </summary>
public class ContentSecurityPolicy
{
private readonly string _baseAndObject = "base-uri 'none'; object-src 'none'";
private readonly Func<INonce, string> policyBuilder;

private readonly CspMode _cspMode;
private readonly bool _strictDynamic;
private readonly bool _unsafeEval;
private readonly string _reportingUri;

/// <summary>
/// Instantiates a new <see cref="ContentSecurityPolicy"/>.
/// </summary>
/// <param name="cspMode">Represents whether the current policy is in enforcing or reporting mode.</param>
/// <param name="strictDynamic">Whether the policy should enable nonce propagation.</param>
/// <param name="unsafeEval">Whether JavaScript's eval should be allowed to run.</param>
/// <param name="reportingUri">An absolute or relative URI representing the reporting endpoint</param>
public ContentSecurityPolicy(
CspMode cspMode,
bool strictDynamic,
bool unsafeEval,
string reportingUri
)
{
_cspMode = cspMode;
_strictDynamic = strictDynamic;
_unsafeEval = unsafeEval;
_reportingUri = reportingUri;

// compute the static directives of the policy up front to avoid doing so on every request
var policyFormat = new StringBuilder()
.Append("script-src")
.Append(" 'nonce-{0}' ") // nonce
.Append(_strictDynamic ? "'strict-dynamic'" : "")
.Append(_unsafeEval ? "'unsafe-eval'" : "")
.Append(" https: http:;") // fall-back allowlist-based CSP for browsers that don't support nonces
.Append(_baseAndObject)
.Append("; ") // end of script-src
.Append(_reportingUri != null ? "report-uri " + _reportingUri : "")
.ToString();

policyBuilder = nonce => string.Format(policyFormat, nonce.GetValue());
}

public string GetHeaderName()
{
return _cspMode == CspMode.REPORTING ? CspConstants.CspReportingHeaderName : CspConstants.CspEnforcedHeaderName;
}
public string GetPolicy(INonce nonce)
{
return policyBuilder.Invoke(nonce);
}
}

public enum CspMode
{
NONE,
REPORTING,
ENFORCING
}
}
88 changes: 88 additions & 0 deletions CSP/src/ContentSecurityPolicyBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Csp
{
/// <summary>
/// Allows customizing content security policies
/// </summary>
public class ContentSecurityPolicyBuilder
{
private CspMode _cspMode;
private bool _strictDynamic;
private bool _unsafeEval;
private string _reportingUri;
private LogLevel _logLevel = LogLevel.Information;

public ContentSecurityPolicyBuilder WithCspMode(CspMode cspMode)
{
_cspMode = cspMode;
return this;
}

public ContentSecurityPolicyBuilder WithStrictDynamic()
{
_strictDynamic = true;
return this;
}

public ContentSecurityPolicyBuilder WithUnsafeEval()
{
_unsafeEval = true;
return this;
}
public ContentSecurityPolicyBuilder WithReportingUri(string reportingUri)
{
// TODO: normalize URL
_reportingUri = reportingUri;
return this;
}

public ContentSecurityPolicyBuilder WithLogLevel(LogLevel logLevel)
{
_logLevel = logLevel;
return this;
}

/// <summary>
/// Whether the policy specifies a relative reporting URI.
/// </summary>
/// <remarks>
/// If this method returns true, a handler for the reporting endpoint will be automatically added to this application.
/// </remarks>
public bool HasLocalReporting()
{
return _reportingUri != null && _reportingUri.StartsWith("/");
}

public CspReportLogger ReportLogger(ICspReportLoggerFactory loggerFactory)
{
return loggerFactory.BuildLogger(_logLevel, _reportingUri);
}

public ContentSecurityPolicy Build()
{
if (_cspMode == CspMode.NONE)
{
// TODO: Error message
throw new InvalidOperationException();
}

if (_cspMode == CspMode.REPORTING && _reportingUri == null)
{
// TODO: Error message
throw new InvalidOperationException();
}

return new ContentSecurityPolicy(
_cspMode,
_strictDynamic,
_unsafeEval,
_reportingUri
);
}
}
}
36 changes: 36 additions & 0 deletions CSP/src/CspConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.AspNetCore.Csp
{
/// <summary>
/// CSP-related constants.
/// </summary>
public static class CspConstants
{
/// <summary>
/// CSP header name in enforcement mode.
/// </summary>
public static readonly string CspEnforcedHeaderName = "Content-Security-Policy";
/// <summary>
/// CSP header name in reporting mode.
/// </summary>
public static readonly string CspReportingHeaderName = "Content-Security-Policy-Report-Only";
/// <summary>
/// Expected content type for requests containing CSP violation reports.
/// </summary>
public static readonly string CspReportContentType = "application/csp-report";
/// <summary>
/// Possible violated directive value used to create textual representations of violation reports.
/// </summary>
public static readonly string ScriptSrcElem = "script-src-elem";
/// <summary>
/// Possible blocked URI value used to create textual representations of violation reports.
/// </summary>
public static readonly string BlockedUriInline = "inline";
/// <summary>
/// Possible violated directive value used to create textual representations of violation reports.
/// </summary>
public static readonly string ScriptSrcAttr = "script-src-attr";
}
}
34 changes: 34 additions & 0 deletions CSP/src/CspMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Csp
{
/// <summary>
/// Middleware for supporting CSP.
/// </summary>
public class CspMiddleware
{
private readonly RequestDelegate _next;
private readonly ContentSecurityPolicy _csp;

/// <summary>
/// Instantiates a new <see cref="CspMiddleware"/>.
/// </summary>
/// <param name="next">The next middleware in the pipeline.</param>
/// <param name="csp">A content security policy generator.</param>
public CspMiddleware(RequestDelegate next, ContentSecurityPolicy csp)
{
_next = next;
_csp = csp;
}

public Task Invoke(HttpContext context, INonce nonce)
{
context.Response.Headers[_csp.GetHeaderName()] = _csp.GetPolicy(nonce);
return _next(context);
}
}
}
61 changes: 61 additions & 0 deletions CSP/src/CspMiddlewareExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Csp
{
/// <summary>
/// Extends <see cref="IApplicationBuilder"/> to add CSP middleware support.
/// </summary>
public static class CspMiddlewareExtensions
{
/// <summary>
/// Adds a CSP middleware to this web application pipeline that will add a custom policy to responses and collect CSP violation reports sent by user agents.
/// </summary>
/// <param name="app">The IApplicationBuilder passed to the Configure method</param>
/// <param name="configurePolicy">A delegate to build a custom content security policy</param>
/// <returns>The original app parameter</returns>
public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action<ContentSecurityPolicyBuilder> configurePolicy)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}

var policyBuilder = new ContentSecurityPolicyBuilder();
configurePolicy(policyBuilder);

if (policyBuilder.HasLocalReporting())
{
var loggerFactory = app.ApplicationServices.GetService<ICspReportLoggerFactory>();
var reportLogger = policyBuilder.ReportLogger(loggerFactory);
app.UseWhen(
context => context.Request.Path.StartsWithSegments(reportLogger.ReportUri),
appBuilder => appBuilder.UseMiddleware<CspReportingMiddleware>(reportLogger));
}

return app.UseMiddleware<CspMiddleware>(policyBuilder.Build());
}

/// <summary>
/// Adds the necessary bindings for CSP. Namely, allows adding nonces to script tags automatically and provides a custom logging factory.
/// </summary>
/// <param name="app">The IApplicationBuilder passed to the Configure method</param>
/// <returns>The original services parameter</returns>
public static IServiceCollection AddCsp(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

services.AddScoped<INonce, Nonce>();
services.AddSingleton<ICspReportLoggerFactory, CspReportLoggerFactory>();

return services;
}
}
}
Loading