Skip to content

Middleware and TagHelpers for CSP support in ASP.NET #24548

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

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
80c137c
Initial CSP Middleware commit
salcho Jul 13, 2020
591cb47
Add test project. Fix project structure. Tests can be built and run a…
salcho Jul 15, 2020
a001197
Revert "Add test project. Fix project structure. Tests can be built a…
salcho Jul 15, 2020
cc530ad
Fix project structure without deleting half of .NET Core
salcho Jul 15, 2020
9456e7c
Add CSP service and first middleware test. Currently failing.
salcho Jul 15, 2020
3b1e8c1
First passing end to end test setting static CSP header
salcho Jul 15, 2020
f18d6e3
Add policy builder and policy tests
salcho Jul 15, 2020
24f933f
Remove CSP service and policy provider
salcho Jul 15, 2020
27335ab
Refactor policy builder to make it easier to register a reporting end…
salcho Jul 15, 2020
5dad8a3
Initial implementation of CSP reporting endpoint
salcho Jul 17, 2020
44de3fd
Improve marshalling of JSON reports to CspReport object
salcho Jul 17, 2020
a82c187
Scaffolding for an example project that demonstrates CSP with templat…
aaronshim Jul 16, 2020
05bba2b
Merge branch 'master' into csp-middleware
salcho Jul 17, 2020
c12b767
Update project structure to reflect changes in upstream
salcho Jul 17, 2020
535088d
Middleware only adds CSP on text/html responses
aaronshim Jul 17, 2020
5b66692
Changing the order of middleware so that pages that use routing still…
aaronshim Jul 17, 2020
52d9e94
Null check during CSP report logging
aaronshim Jul 17, 2020
c585d1f
Initial implementation/scaffolding for TagHelpers to auto-nonce scrip…
aaronshim Jul 17, 2020
511f74b
Attempt at scaffolding an integration test to check that the template…
aaronshim Jul 18, 2020
a7e4a7a
Compute CSP up front. Only set up reporting endpoint if report URI is…
salcho Jul 20, 2020
2e1f223
Refactor CSP logging and add test
salcho Jul 20, 2020
39fff82
Add CSP header on all requests
salcho Jul 20, 2020
38424fa
Code cleanup
salcho Jul 21, 2020
4e51fe7
Textualize CSP reports depending on the current log level. Add tests …
salcho Jul 22, 2020
22feb96
Remove debug nonce
salcho Jul 23, 2020
fa5dbfa
Add documentation
salcho Jul 23, 2020
d6eaa92
Simplify sample site.
aaronshim Jul 24, 2020
48a9377
Merge remote-tracking branch 'msft/master' into csp-middleware
aaronshim Jul 31, 2020
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
172 changes: 110 additions & 62 deletions AspNetCore.sln

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<ProjectReferenceProvider Include="Microsoft.Extensions.Logging.AzureAppServices" ProjectPath="$(RepoRoot)src\Logging.AzureAppServices\src\Microsoft.Extensions.Logging.AzureAppServices.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ConcurrencyLimiter" ProjectPath="$(RepoRoot)src\Middleware\ConcurrencyLimiter\src\Microsoft.AspNetCore.ConcurrencyLimiter.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Cors" ProjectPath="$(RepoRoot)src\Middleware\CORS\src\Microsoft.AspNetCore.Cors.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Csp" ProjectPath="$(RepoRoot)src\Middleware\CSP\src\Microsoft.AspNetCore.Csp.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.Abstractions" ProjectPath="$(RepoRoot)src\Middleware\Diagnostics.Abstractions\src\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" ProjectPath="$(RepoRoot)src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics" ProjectPath="$(RepoRoot)src\Middleware\Diagnostics\src\Microsoft.AspNetCore.Diagnostics.csproj" />
Expand Down
1 change: 1 addition & 0 deletions eng/SharedFramework.Local.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<AspNetCoreAppReferenceAndPackage Include="Microsoft.Extensions.Identity.Stores" />
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Connections.Abstractions" />
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Authorization" />
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Csp" />
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Http.Connections.Common" />
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.SignalR.Protocols.Json" />
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.SignalR.Common" />
Expand Down
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",
]
}
}
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;
}
}
}
65 changes: 65 additions & 0 deletions src/Middleware/CSP/src/CspReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Microsoft.AspNetCore.Csp
{
/// <summary>
/// An object representing a CSP violation report.
/// </summary>
public class CspReport
{
[JsonPropertyName("csp-report")]
public Report ReportData { get; set; }
public class Report
{
[JsonPropertyName("blocked-uri")]
public string BlockedUri { get; set; }
[JsonPropertyName("document-uri")]
public string DocumentUri { get; set; }
[JsonPropertyName("referrer")]
public string Referrer { get; set; }
[JsonPropertyName("violated-directive")]
public string ViolatedDirective { get; set; }
[JsonPropertyName("source-file")]
public string SourceFile { get; set; }
[JsonPropertyName("line-number")]
[JsonConverter(typeof(NumberToStringConverter))]
public string LineNumber { get; set; }

// Old browsers don't set the next two fields (e.g. Firefox v25/v26)
[JsonPropertyName("original-policy")]
public string OriginalPolicy { get; set; }
[JsonPropertyName("effective-directive")]
public string EffectiveDirective { get; set; }

// CSP3 only
[JsonPropertyName("script-sample")]
public string ScriptSample { get; set; }
[JsonPropertyName("disposition")]
public string Disposition { get; set; }
}
}

class NumberToStringConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
return reader.GetInt32().ToString();
}

return reader.GetString();
}

public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
}
Loading