Skip to content

Commit

Permalink
Eliminate the use of inline styles, making it compatible with strict …
Browse files Browse the repository at this point in the history
…CSP for style. (#634)

Thank you for making this project easy to build and fork.
With this PR MiniProfiler will run under a strict CSP that disallows inline styles and scripts (by using nonce) with zero errors.

It accomplish this by putting dynamically generated style tag values as data attributes, and then later after appending the miniprofiler html, it queries for them and manipulates the style object on Element directly, thus eliminating the need for inline style.

Co-authored-by: Nick Craver <nrcraver@gmail.com>
  • Loading branch information
rwasef1830 and NickCraver authored Aug 3, 2023
1 parent b53cf08 commit 921601f
Show file tree
Hide file tree
Showing 16 changed files with 126 additions and 35 deletions.
2 changes: 2 additions & 0 deletions docs/Releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ layout: "default"
This page tracks major changes included in any update starting with version 4.0.0.3

#### Unreleased
- **New**:
- Support for strict CSP (dynamic inline styles removed) ([#634](https://github.com/MiniProfiler/dotnet/pull/634) - thanks [rwasef1830](https://github.com/rwasef1830))
- **Fixes/Changes**:
- Upgraded MongoDB driver, allowing automatic index creation and profiler expiration ([#613](https://github.com/MiniProfiler/dotnet/pull/613) - thanks [IanKemp](https://github.com/IanKemp))

Expand Down
20 changes: 20 additions & 0 deletions samples/Samples.AspNet/Helpers/NonceService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Samples.AspNetCore
{
/// <summary>
/// Nonce service (custom implementation) for sharing a random nonce for the lifetime of a request.
/// </summary>
public class NonceService
{
public string RequestNonce { get; } = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
}

public static class NonceExtensions
{
public static string? GetNonce(this HttpContext context) => context.RequestServices.GetService<NonceService>()?.RequestNonce;
}
}
2 changes: 1 addition & 1 deletion samples/Samples.AspNet/Pages/RazorPagesSample.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<partial name="Index.RightPanel" />
</div>
@section scripts {
<script>
<script nonce="@HttpContext.GetNonce()">
$(function () {
// these links should fire ajax requests, not do navigation
$('.ajax-requests a').click(function () {
Expand Down
12 changes: 12 additions & 0 deletions samples/Samples.AspNet/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public void ConfigureServices(IServiceCollection services)
options.SuppressAsyncSuffixInActionNames = false;
});

// Registering a per-request Nonce provider for use in headers and scripts - this is optional, only demonstrating.
services.AddScoped<NonceService>();

// Add MiniProfiler services
// If using Entity Framework Core, add profiling for it as well (see the end)
// Note .AddMiniProfiler() returns a IMiniProfilerBuilder for easy IntelliSense
Expand Down Expand Up @@ -110,6 +113,8 @@ public void ConfigureServices(IServiceCollection services)
options.IgnoredPaths.Add("/lib");
options.IgnoredPaths.Add("/css");
options.IgnoredPaths.Add("/js");
options.NonceProvider = s => s.GetService<NonceService>()?.RequestNonce;
}).AddEntityFramework();
}

Expand All @@ -128,6 +133,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseMiniProfiler()
.UseStaticFiles()
.UseRouting()
// Demonstrating CSP support, this is not required.
.Use(async (context, next) =>
{
var nonce = context.RequestServices.GetService<NonceService>()?.RequestNonce;
context.Response.Headers.Add("Content-Security-Policy", $"script-src 'self' 'nonce-{nonce}'");
await next();
})
.UseEndpoints(endpoints =>
{
endpoints.MapAreaControllerRoute("areaRoute", "MySpace",
Expand Down
2 changes: 1 addition & 1 deletion samples/Samples.AspNet/Views/Shared/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<partial name="Index.RightPanel" />
</div>
@section scripts {
<script>
<script nonce="@Context.GetNonce()">
$(function () {
// these links should fire ajax requests, not do navigation
$('.ajax-requests a').click(function () {
Expand Down
2 changes: 1 addition & 1 deletion samples/Samples.AspNet/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
@RenderSection("scripts", required: false)

@* Simple options are exposed...or make a full options class for customizing. *@
<mini-profiler position="@RenderPosition.Right" max-traces="5" color-scheme="ColorScheme.Auto" nonce="45" decimal-places="2" />
<mini-profiler position="@RenderPosition.Right" max-traces="5" color-scheme="ColorScheme.Auto" decimal-places="2" />
@*<mini-profiler options="new RenderOptions { Position = RenderPosition.Right, MaxTracesToShow = 5, ColorScheme = ColorScheme.Auto }" />*@
</body>
</html>
1 change: 1 addition & 0 deletions src/MiniProfiler.AspNetCore/MiniProfilerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static HtmlString RenderIncludes(
path: context.Request.PathBase + path,
isAuthorized: state?.IsAuthorized ?? false,
renderOptions,
context.RequestServices,
requestIDs: state?.RequestIDs);

return new HtmlString(result);
Expand Down
4 changes: 2 additions & 2 deletions src/MiniProfiler.AspNetCore/MiniProfilerMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ private async Task<string> ResultsIndexAsync(HttpContext context)
context.Response.ContentType = "text/html; charset=utf-8";

var path = context.Request.PathBase + Options.RouteBasePath.Value.EnsureTrailingSlash();
return Render.ResultListHtml(Options, path);
return Render.ResultListHtml(Options, context.RequestServices, path);
}

/// <summary>
Expand Down Expand Up @@ -406,7 +406,7 @@ private async Task<string> ResultsListAsync(HttpContext context)
else
{
context.Response.ContentType = "text/html; charset=utf-8";
return Render.SingleResultHtml(profiler, context.Request.PathBase + Options.RouteBasePath.Value.EnsureTrailingSlash());
return Render.SingleResultHtml(profiler, context.RequestServices, context.Request.PathBase + Options.RouteBasePath.Value.EnsureTrailingSlash());
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ public class MiniProfilerBaseOptions
/// </summary>
public Func<Timing, IDisposable>? TimingInstrumentationProvider { get; set; }

/// <summary>
/// Called whenever a nonce is required for a script or style tag for each request.
/// </summary>
public Func<IServiceProvider, string?> NonceProvider { get; set; } = _ => null;

/// <summary>
/// Called when passed to <see cref="MiniProfiler.Configure{T}(T)"/>.
/// </summary>
Expand Down
48 changes: 38 additions & 10 deletions src/MiniProfiler.Shared/Internal/Render.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Web;

namespace StackExchange.Profiling.Internal
{
Expand All @@ -17,12 +18,14 @@ public static class Render
/// <param name="path">The root path that MiniProfiler is being served from.</param>
/// <param name="isAuthorized">Whether the current user is authorized for MiniProfiler.</param>
/// <param name="renderOptions">The option overrides (if any) to use rendering this MiniProfiler.</param>
/// <param name="serviceProvider">The current request service provider.</param>
/// <param name="requestIDs">The request IDs to fetch for this render.</param>
public static string Includes(
MiniProfiler profiler,
string path,
bool isAuthorized,
RenderOptions renderOptions,
RenderOptions? renderOptions,
IServiceProvider? serviceProvider = null,
List<Guid>? requestIDs = null)
{
var sb = StringBuilderCache.Get();
Expand Down Expand Up @@ -84,9 +87,12 @@ public static string Includes(
{
sb.Append(" data-start-hidden=\"true\"");
}
if (renderOptions?.Nonce.HasValue() ?? false)

var nonce = renderOptions?.Nonce ??
(serviceProvider != null ? profiler.Options.NonceProvider?.Invoke(serviceProvider) : null);
if (nonce?.HasValue() ?? false)
{
sb.Append(" nonce=\"").Append(System.Web.HttpUtility.HtmlAttributeEncode(renderOptions.Nonce)).Append("\"");
sb.Append(" nonce=\"").Append(HttpUtility.HtmlAttributeEncode(nonce)).Append("\"");
}

sb.Append(" data-max-traces=\"");
Expand Down Expand Up @@ -131,6 +137,7 @@ public static string Includes(
/// <param name="maxTracesToShow">The maximum number of profilers to show (before the oldest is removed - defaults to <see cref="MiniProfilerBaseOptions.PopupMaxTracesToShow"/>).</param>
/// <param name="showControls">Whether to show the controls (defaults to <see cref="MiniProfilerBaseOptions.ShowControls"/>).</param>
/// <param name="startHidden">Whether to start hidden (defaults to <see cref="MiniProfilerBaseOptions.PopupStartHidden"/>).</param>
/// <param name="nonce">Content script policy nonce value to use for script and style tags generated.</param>
public static string Includes(
MiniProfiler profiler,
string path,
Expand All @@ -141,7 +148,8 @@ public static string Includes(
bool? showTimeWithChildren = null,
int? maxTracesToShow = null,
bool? showControls = null,
bool? startHidden = null)
bool? startHidden = null,
string? nonce = null)
{
var sb = StringBuilderCache.Get();
var options = profiler.Options;
Expand All @@ -150,6 +158,12 @@ public static string Includes(
sb.Append(path);
sb.Append("includes.min.js?v=");
sb.Append(options.VersionHash);

if (!string.IsNullOrWhiteSpace(nonce))
{
sb.Append("\" nonce=\"");
sb.Append(HttpUtility.HtmlAttributeEncode(nonce));
}
sb.Append("\" data-version=\"");
sb.Append(options.VersionHash);
sb.Append("\" data-path=\"");
Expand Down Expand Up @@ -233,19 +247,30 @@ public static string Includes(
/// Renders a full HTML page for the share link in MiniProfiler.
/// </summary>
/// <param name="profiler">The profiler to render a tag for.</param>
/// <param name="serviceProvider">The current request service provider.</param>
/// <param name="path">The root path that MiniProfiler is being served from.</param>
/// <returns>A full HTML page for this MiniProfiler.</returns>
public static string SingleResultHtml(MiniProfiler profiler, string path)
public static string SingleResultHtml(MiniProfiler profiler, IServiceProvider serviceProvider, string path)
{
var sb = StringBuilderCache.Get();
sb.Append("<html><head><title>");
sb.Append(profiler.Name);
sb.Append(" (");
sb.Append(profiler.DurationMilliseconds.ToString(CultureInfo.InvariantCulture));
sb.Append(" ms) - Profiling Results</title><script>var profiler = ");
sb.Append(" ms) - Profiling Results</title><script");

var nonce = profiler.Options.NonceProvider?.Invoke(serviceProvider) ?? string.Empty;
if (!string.IsNullOrWhiteSpace(nonce))
{
sb.Append(" nonce=\"");
sb.Append(HttpUtility.HtmlAttributeEncode(nonce));
sb.Append("\"");
}

sb.Append(">var profiler = ");
sb.Append(profiler.ToJson(htmlEscape: true));
sb.Append(";</script>");
sb.Append(Includes(profiler, path: path, isAuthorized: true));
sb.Append(Includes(profiler, path: path, isAuthorized: true, nonce: nonce));
sb.Append(@"</head><body><div class=""mp-result-full""></div></body></html>");
return sb.ToString();
}
Expand All @@ -254,17 +279,20 @@ public static string SingleResultHtml(MiniProfiler profiler, string path)
/// Renders a full HTML page for the share link in MiniProfiler.
/// </summary>
/// <param name="options">The options to render for.</param>
/// <param name="serviceProvider">The current request service provider.</param>
/// <param name="path">The root path that MiniProfiler is being served from.</param>
/// <returns>A full HTML page for this MiniProfiler.</returns>
public static string ResultListHtml(MiniProfilerBaseOptions options, string path)
public static string ResultListHtml(MiniProfilerBaseOptions options, IServiceProvider serviceProvider, string path)
{
var version = options.VersionHash;
var nonce = options.NonceProvider?.Invoke(serviceProvider) ?? string.Empty;
var nonceAttribute = !string.IsNullOrWhiteSpace(nonce) ? " nonce=\"" + HttpUtility.HtmlAttributeEncode(nonce) + "\"" : null;
return $@"<html>
<head>
<title>List of profiling sessions</title>
<script id=""mini-profiler"" data-ids="""" src=""{path}includes.min.js?v={version}""></script>
<script{nonceAttribute} id=""mini-profiler"" data-ids="""" src=""{path}includes.min.js?v={version}""></script>
<link href=""{path}includes.min.css?v={version}"" rel=""stylesheet"" />
<script>MiniProfiler.listInit({{path: '{path}', version: '{version}', colorScheme: '{options.ColorScheme}'}});</script>
<script{nonceAttribute}>MiniProfiler.listInit({{path: '{path}', version: '{version}', colorScheme: '{options.ColorScheme}'}});</script>
</head>
<body>
<table class=""mp-results-index"">
Expand Down
2 changes: 1 addition & 1 deletion src/MiniProfiler.Shared/MiniProfiler.Shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.0.0" />

<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" PrivateAssets="all" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" PrivateAssets="all" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="3.7.2" PrivateAssets="all" />
</ItemGroup>

Expand Down
3 changes: 3 additions & 0 deletions src/MiniProfiler.Shared/ui/includes.css
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@
.mp-result table.mp-client-timings {
margin-top: 10px;
}
.mp-result table.mp-client-timings th:first-child {
text-align: left;
}
.mp-result table.mp-client-timings td:nth-child(2) {
width: 100%;
padding: 0;
Expand Down
7 changes: 5 additions & 2 deletions src/MiniProfiler.Shared/ui/includes.less
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
}

.mp-scheme-light {
color-scheme: light;
color-scheme: light;
}

.mp-scheme-dark {
Expand All @@ -72,7 +72,6 @@
--mp-highlight-keyword-color: #36a1ef;
/* Borders */
--mp-result-border: solid 0.5px #575757;

color-scheme: dark;

body {
Expand Down Expand Up @@ -154,6 +153,10 @@
table.mp-client-timings {
margin-top: 10px;

th:first-child {
text-align: left;
}

td:nth-child(2) {
width: 100%;
padding: 0;
Expand Down
Loading

0 comments on commit 921601f

Please sign in to comment.