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

Merged
merged 5 commits into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from 4 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 src/Middleware/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",
]
}
}
34 changes: 34 additions & 0 deletions src/Middleware/CSP/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# CSP
gchatz22 marked this conversation as resolved.
Show resolved Hide resolved

## 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.

There was a previous discussion about CSP in .NET [here](https://github.com/dotnet/aspnetcore/issues/6001), that we have considered for our design.
gchatz22 marked this conversation as resolved.
Show resolved Hide resolved

## 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 thepolicy to HTTP responses according to the configuration.
gchatz22 marked this conversation as resolved.
Show resolved Hide resolved
* 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.

## Authors
* Co-authored-by: Aaron Shim - aaronshim@google.com
* Co-authored-by: Santiago Diaz - salchoman@gmail.com
gchatz22 marked this conversation as resolved.
Show resolved Hide resolved
72 changes: 72 additions & 0 deletions src/Middleware/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 src/Middleware/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 src/Middleware/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 src/Middleware/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 src/Middleware/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