Skip to content

Commit

Permalink
Middleware and TagHelpers for CSP support in ASP.NET (#1)
Browse files Browse the repository at this point in the history
Hello .NET community!

This PR adds Content Security Policy support for ASP.NET as middleware. CSP is a 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.

Summary of the changes (Less than 80 chars)

* 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 thepolicy 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.
* With these tools, developers can enable CSP in reporting mode, collect reports and identify and refactor existing code that is incompatible with CSP from these reports. Finally, developers will be able to switch CSP to enforcing mode, which will provide a very robust defense against XSS.

Addresses dotnet/aspnetcore#6001

Co-authored with: Aaron Shim - aaronshim@google.com
  • Loading branch information
gchatz22 authored Aug 13, 2020
1 parent 9a526e1 commit fd52908
Show file tree
Hide file tree
Showing 38 changed files with 1,665 additions and 0 deletions.
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 - barry.dorrans@microsoft.com

## Authors
* Co-authored-by: Aaron Shim - aaronshim@google.com
* Co-authored-by: Santiago Diaz - salchoman@gmail.com
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

0 comments on commit fd52908

Please sign in to comment.