Skip to content

Commit 921601f

Browse files
Eliminate the use of inline styles, making it compatible with strict 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 <[email protected]>
1 parent b53cf08 commit 921601f

File tree

16 files changed

+126
-35
lines changed

16 files changed

+126
-35
lines changed

docs/Releases.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ layout: "default"
66
This page tracks major changes included in any update starting with version 4.0.0.3
77

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using System.Security.Cryptography;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace Samples.AspNetCore
7+
{
8+
/// <summary>
9+
/// Nonce service (custom implementation) for sharing a random nonce for the lifetime of a request.
10+
/// </summary>
11+
public class NonceService
12+
{
13+
public string RequestNonce { get; } = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
14+
}
15+
16+
public static class NonceExtensions
17+
{
18+
public static string? GetNonce(this HttpContext context) => context.RequestServices.GetService<NonceService>()?.RequestNonce;
19+
}
20+
}

samples/Samples.AspNet/Pages/RazorPagesSample.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<partial name="Index.RightPanel" />
1414
</div>
1515
@section scripts {
16-
<script>
16+
<script nonce="@HttpContext.GetNonce()">
1717
$(function () {
1818
// these links should fire ajax requests, not do navigation
1919
$('.ajax-requests a').click(function () {

samples/Samples.AspNet/Startup.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public void ConfigureServices(IServiceCollection services)
4444
options.SuppressAsyncSuffixInActionNames = false;
4545
});
4646

47+
// Registering a per-request Nonce provider for use in headers and scripts - this is optional, only demonstrating.
48+
services.AddScoped<NonceService>();
49+
4750
// Add MiniProfiler services
4851
// If using Entity Framework Core, add profiling for it as well (see the end)
4952
// Note .AddMiniProfiler() returns a IMiniProfilerBuilder for easy IntelliSense
@@ -110,6 +113,8 @@ public void ConfigureServices(IServiceCollection services)
110113
options.IgnoredPaths.Add("/lib");
111114
options.IgnoredPaths.Add("/css");
112115
options.IgnoredPaths.Add("/js");
116+
117+
options.NonceProvider = s => s.GetService<NonceService>()?.RequestNonce;
113118
}).AddEntityFramework();
114119
}
115120

@@ -128,6 +133,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
128133
app.UseMiniProfiler()
129134
.UseStaticFiles()
130135
.UseRouting()
136+
// Demonstrating CSP support, this is not required.
137+
.Use(async (context, next) =>
138+
{
139+
var nonce = context.RequestServices.GetService<NonceService>()?.RequestNonce;
140+
context.Response.Headers.Add("Content-Security-Policy", $"script-src 'self' 'nonce-{nonce}'");
141+
await next();
142+
})
131143
.UseEndpoints(endpoints =>
132144
{
133145
endpoints.MapAreaControllerRoute("areaRoute", "MySpace",

samples/Samples.AspNet/Views/Shared/Index.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<partial name="Index.RightPanel" />
1212
</div>
1313
@section scripts {
14-
<script>
14+
<script nonce="@Context.GetNonce()">
1515
$(function () {
1616
// these links should fire ajax requests, not do navigation
1717
$('.ajax-requests a').click(function () {

samples/Samples.AspNet/Views/Shared/_Layout.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
@RenderSection("scripts", required: false)
4848

4949
@* Simple options are exposed...or make a full options class for customizing. *@
50-
<mini-profiler position="@RenderPosition.Right" max-traces="5" color-scheme="ColorScheme.Auto" nonce="45" decimal-places="2" />
50+
<mini-profiler position="@RenderPosition.Right" max-traces="5" color-scheme="ColorScheme.Auto" decimal-places="2" />
5151
@*<mini-profiler options="new RenderOptions { Position = RenderPosition.Right, MaxTracesToShow = 5, ColorScheme = ColorScheme.Auto }" />*@
5252
</body>
5353
</html>

src/MiniProfiler.AspNetCore/MiniProfilerExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public static HtmlString RenderIncludes(
3535
path: context.Request.PathBase + path,
3636
isAuthorized: state?.IsAuthorized ?? false,
3737
renderOptions,
38+
context.RequestServices,
3839
requestIDs: state?.RequestIDs);
3940

4041
return new HtmlString(result);

src/MiniProfiler.AspNetCore/MiniProfilerMiddleware.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ private async Task<string> ResultsIndexAsync(HttpContext context)
291291
context.Response.ContentType = "text/html; charset=utf-8";
292292

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

297297
/// <summary>
@@ -406,7 +406,7 @@ private async Task<string> ResultsListAsync(HttpContext context)
406406
else
407407
{
408408
context.Response.ContentType = "text/html; charset=utf-8";
409-
return Render.SingleResultHtml(profiler, context.Request.PathBase + Options.RouteBasePath.Value.EnsureTrailingSlash());
409+
return Render.SingleResultHtml(profiler, context.RequestServices, context.Request.PathBase + Options.RouteBasePath.Value.EnsureTrailingSlash());
410410
}
411411
}
412412
}

src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@ public class MiniProfilerBaseOptions
227227
/// </summary>
228228
public Func<Timing, IDisposable>? TimingInstrumentationProvider { get; set; }
229229

230+
/// <summary>
231+
/// Called whenever a nonce is required for a script or style tag for each request.
232+
/// </summary>
233+
public Func<IServiceProvider, string?> NonceProvider { get; set; } = _ => null;
234+
230235
/// <summary>
231236
/// Called when passed to <see cref="MiniProfiler.Configure{T}(T)"/>.
232237
/// </summary>

src/MiniProfiler.Shared/Internal/Render.cs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Globalization;
4+
using System.Web;
45

56
namespace StackExchange.Profiling.Internal
67
{
@@ -17,12 +18,14 @@ public static class Render
1718
/// <param name="path">The root path that MiniProfiler is being served from.</param>
1819
/// <param name="isAuthorized">Whether the current user is authorized for MiniProfiler.</param>
1920
/// <param name="renderOptions">The option overrides (if any) to use rendering this MiniProfiler.</param>
21+
/// <param name="serviceProvider">The current request service provider.</param>
2022
/// <param name="requestIDs">The request IDs to fetch for this render.</param>
2123
public static string Includes(
2224
MiniProfiler profiler,
2325
string path,
2426
bool isAuthorized,
25-
RenderOptions renderOptions,
27+
RenderOptions? renderOptions,
28+
IServiceProvider? serviceProvider = null,
2629
List<Guid>? requestIDs = null)
2730
{
2831
var sb = StringBuilderCache.Get();
@@ -84,9 +87,12 @@ public static string Includes(
8487
{
8588
sb.Append(" data-start-hidden=\"true\"");
8689
}
87-
if (renderOptions?.Nonce.HasValue() ?? false)
90+
91+
var nonce = renderOptions?.Nonce ??
92+
(serviceProvider != null ? profiler.Options.NonceProvider?.Invoke(serviceProvider) : null);
93+
if (nonce?.HasValue() ?? false)
8894
{
89-
sb.Append(" nonce=\"").Append(System.Web.HttpUtility.HtmlAttributeEncode(renderOptions.Nonce)).Append("\"");
95+
sb.Append(" nonce=\"").Append(HttpUtility.HtmlAttributeEncode(nonce)).Append("\"");
9096
}
9197

9298
sb.Append(" data-max-traces=\"");
@@ -131,6 +137,7 @@ public static string Includes(
131137
/// <param name="maxTracesToShow">The maximum number of profilers to show (before the oldest is removed - defaults to <see cref="MiniProfilerBaseOptions.PopupMaxTracesToShow"/>).</param>
132138
/// <param name="showControls">Whether to show the controls (defaults to <see cref="MiniProfilerBaseOptions.ShowControls"/>).</param>
133139
/// <param name="startHidden">Whether to start hidden (defaults to <see cref="MiniProfilerBaseOptions.PopupStartHidden"/>).</param>
140+
/// <param name="nonce">Content script policy nonce value to use for script and style tags generated.</param>
134141
public static string Includes(
135142
MiniProfiler profiler,
136143
string path,
@@ -141,7 +148,8 @@ public static string Includes(
141148
bool? showTimeWithChildren = null,
142149
int? maxTracesToShow = null,
143150
bool? showControls = null,
144-
bool? startHidden = null)
151+
bool? startHidden = null,
152+
string? nonce = null)
145153
{
146154
var sb = StringBuilderCache.Get();
147155
var options = profiler.Options;
@@ -150,6 +158,12 @@ public static string Includes(
150158
sb.Append(path);
151159
sb.Append("includes.min.js?v=");
152160
sb.Append(options.VersionHash);
161+
162+
if (!string.IsNullOrWhiteSpace(nonce))
163+
{
164+
sb.Append("\" nonce=\"");
165+
sb.Append(HttpUtility.HtmlAttributeEncode(nonce));
166+
}
153167
sb.Append("\" data-version=\"");
154168
sb.Append(options.VersionHash);
155169
sb.Append("\" data-path=\"");
@@ -233,19 +247,30 @@ public static string Includes(
233247
/// Renders a full HTML page for the share link in MiniProfiler.
234248
/// </summary>
235249
/// <param name="profiler">The profiler to render a tag for.</param>
250+
/// <param name="serviceProvider">The current request service provider.</param>
236251
/// <param name="path">The root path that MiniProfiler is being served from.</param>
237252
/// <returns>A full HTML page for this MiniProfiler.</returns>
238-
public static string SingleResultHtml(MiniProfiler profiler, string path)
253+
public static string SingleResultHtml(MiniProfiler profiler, IServiceProvider serviceProvider, string path)
239254
{
240255
var sb = StringBuilderCache.Get();
241256
sb.Append("<html><head><title>");
242257
sb.Append(profiler.Name);
243258
sb.Append(" (");
244259
sb.Append(profiler.DurationMilliseconds.ToString(CultureInfo.InvariantCulture));
245-
sb.Append(" ms) - Profiling Results</title><script>var profiler = ");
260+
sb.Append(" ms) - Profiling Results</title><script");
261+
262+
var nonce = profiler.Options.NonceProvider?.Invoke(serviceProvider) ?? string.Empty;
263+
if (!string.IsNullOrWhiteSpace(nonce))
264+
{
265+
sb.Append(" nonce=\"");
266+
sb.Append(HttpUtility.HtmlAttributeEncode(nonce));
267+
sb.Append("\"");
268+
}
269+
270+
sb.Append(">var profiler = ");
246271
sb.Append(profiler.ToJson(htmlEscape: true));
247272
sb.Append(";</script>");
248-
sb.Append(Includes(profiler, path: path, isAuthorized: true));
273+
sb.Append(Includes(profiler, path: path, isAuthorized: true, nonce: nonce));
249274
sb.Append(@"</head><body><div class=""mp-result-full""></div></body></html>");
250275
return sb.ToString();
251276
}
@@ -254,17 +279,20 @@ public static string SingleResultHtml(MiniProfiler profiler, string path)
254279
/// Renders a full HTML page for the share link in MiniProfiler.
255280
/// </summary>
256281
/// <param name="options">The options to render for.</param>
282+
/// <param name="serviceProvider">The current request service provider.</param>
257283
/// <param name="path">The root path that MiniProfiler is being served from.</param>
258284
/// <returns>A full HTML page for this MiniProfiler.</returns>
259-
public static string ResultListHtml(MiniProfilerBaseOptions options, string path)
285+
public static string ResultListHtml(MiniProfilerBaseOptions options, IServiceProvider serviceProvider, string path)
260286
{
261287
var version = options.VersionHash;
288+
var nonce = options.NonceProvider?.Invoke(serviceProvider) ?? string.Empty;
289+
var nonceAttribute = !string.IsNullOrWhiteSpace(nonce) ? " nonce=\"" + HttpUtility.HtmlAttributeEncode(nonce) + "\"" : null;
262290
return $@"<html>
263291
<head>
264292
<title>List of profiling sessions</title>
265-
<script id=""mini-profiler"" data-ids="""" src=""{path}includes.min.js?v={version}""></script>
293+
<script{nonceAttribute} id=""mini-profiler"" data-ids="""" src=""{path}includes.min.js?v={version}""></script>
266294
<link href=""{path}includes.min.css?v={version}"" rel=""stylesheet"" />
267-
<script>MiniProfiler.listInit({{path: '{path}', version: '{version}', colorScheme: '{options.ColorScheme}'}});</script>
295+
<script{nonceAttribute}>MiniProfiler.listInit({{path: '{path}', version: '{version}', colorScheme: '{options.ColorScheme}'}});</script>
268296
</head>
269297
<body>
270298
<table class=""mp-results-index"">

0 commit comments

Comments
 (0)